@cryptotaxi247 / netdata-1 / commits / cc0502ab9

Model Context Protocol Server (MCP) for Netdata (#20244)

* websocket server implementation, integrated to the web server. - Full support for RFC 6455 (WebSocket Protocol) - Compression via the permessage-deflate extension (RFC 7692) - Supports restricted window sizes, close codes, and fragmentation - Passes successfully the entire Autobahn Test Suite - Fast: adaptive buffers for zero malloc/free during processing * websocket server implementation, integrated to the web server. - Full support for RFC 6455 (WebSocket Protocol) - Compression via the permessage-deflate extension (RFC 7692) - Supports restricted window sizes, close codes, and fragmentation - Passes successfully the entire Autobahn Test Suite - Fast: adaptive buffers for zero malloc/free during processing * add counters to poll-events to find the cause of infinite loop * added jsonrpc framework to websocket server * prototype mcp over websocket * fix formatting * completed mcp initialize * changed the interface of mcp to follow the same pattern all netdata APIs do * added resource and resourse template for contexts * implemented context categories * use the first dot on contexts * in RRDSTATS_RETENTION use just the contents of the response to figure out if the tier is used * delete mcp prototype * reset websocket window bits to zero of web_client reset * do not enable the websocket server, unless internal checks are enabled

Costa Tsaousis committed May 15, 2025 at 14:51 UTC cc0502ab96db3fb11a71603ef27099055efbe329
69 files changed +10322 -157
.gitignore
+2
@@ -195,3 +195,5 @@ packaging/tools/agent-events/parseBehaviorTest.go
195 packaging/tools/agent-events/server
196 packaging/tools/agent-events/go.mod
197 packaging/tools/agent-events/go.sum
198 +
199 +**/.claude/settings.local.json
CMakeLists.txt
+51 -12
@@ -1621,6 +1621,8 @@ set(RRD_PLUGIN_FILES
1621 src/database/rrd.h
1622 src/database/rrd-metadata.c
1623 src/database/rrd-metadata.h
1624 + src/database/rrd-retention.c
1625 + src/database/rrd-retention.h
1626 src/database/rrdset.c
1627 src/database/storage-engine.c
1628 src/database/storage-engine.h
@@ -1690,6 +1692,8 @@ set(RRD_PLUGIN_FILES
1692 src/database/pattern-array.c
1693 src/database/pattern-array.h
1694 src/database/contexts/rrdcontext-queues.c
1695 + src/database/contexts/rrdcontext-context-registry.c
1696 + src/database/contexts/rrdcontext-context-registry.h
1697 )
1698
1699 if(ENABLE_DBENGINE)
@@ -1825,22 +1829,57 @@ set(STREAMING_PLUGIN_FILES
1829 )
1830
1831 set(WEB_PLUGIN_FILES
1828 - src/web/server/web_client.c
1829 - src/web/server/web_client.h
1830 - src/web/server/web_server.c
1831 - src/web/server/web_server.h
1832 - src/web/server/static/static-threaded.c
1833 - src/web/server/static/static-threaded.h
1834 - src/web/server/web_client_cache.c
1835 - src/web/server/web_client_cache.h
1836 - src/web/api/v3/api_v3_stream_info.c
1837 - src/web/api/v3/api_v3_stream_path.c
1838 - src/web/api/queries/backfill.c
1839 - src/web/api/queries/backfill.h
1832 src/web/api/functions/function-metrics-cardinality.c
1833 src/web/api/functions/function-metrics-cardinality.h
1834 + src/web/api/queries/backfill.c
1835 + src/web/api/queries/backfill.h
1836 src/web/api/request_source.c
1837 src/web/api/request_source.h
1838 + src/web/api/v3/api_v3_stream_info.c
1839 + src/web/api/v3/api_v3_stream_path.c
1840 + src/web/mcp/adapters/mcp-websocket.c
1841 + src/web/mcp/adapters/mcp-websocket.h
1842 + src/web/mcp/mcp-context.c
1843 + src/web/mcp/mcp-context.h
1844 + src/web/mcp/mcp-initialize.c
1845 + src/web/mcp/mcp-initialize.h
1846 + src/web/mcp/mcp-notifications.c
1847 + src/web/mcp/mcp-notifications.h
1848 + src/web/mcp/mcp-prompts.c
1849 + src/web/mcp/mcp-prompts.h
1850 + src/web/mcp/mcp-resources.c
1851 + src/web/mcp/mcp-resources.h
1852 + src/web/mcp/mcp-system.c
1853 + src/web/mcp/mcp-system.h
1854 + src/web/mcp/mcp-tools.c
1855 + src/web/mcp/mcp-tools.h
1856 + src/web/mcp/mcp.c
1857 + src/web/mcp/mcp.h
1858 + src/web/server/static/static-threaded.c
1859 + src/web/server/static/static-threaded.h
1860 + src/web/server/web_client.c
1861 + src/web/server/web_client.h
1862 + src/web/server/web_client_cache.c
1863 + src/web/server/web_client_cache.h
1864 + src/web/server/web_server.c
1865 + src/web/server/web_server.h
1866 + src/web/websocket/websocket-buffer.h
1867 + src/web/websocket/websocket-compression.c
1868 + src/web/websocket/websocket-compression.h
1869 + src/web/websocket/websocket-echo.c
1870 + src/web/websocket/websocket-echo.h
1871 + src/web/websocket/websocket-handshake.c
1872 + src/web/websocket/websocket-internal.h
1873 + src/web/websocket/websocket-jsonrpc.c
1874 + src/web/websocket/websocket-jsonrpc.h
1875 + src/web/websocket/websocket-message.c
1876 + src/web/websocket/websocket-receive.c
1877 + src/web/websocket/websocket-send.c
1878 + src/web/websocket/websocket-thread.c
1879 + src/web/websocket/websocket-thread.h
1880 + src/web/websocket/websocket-utils.c
1881 + src/web/websocket/websocket.c
1882 + src/web/websocket/websocket.h
1883 )
1884
1885 set(CLAIM_PLUGIN_FILES
src/daemon/daemon-shutdown.c
+2
@@ -23,6 +23,7 @@ void rrd_functions_inflight_destroy(void);
23 void cgroup_netdev_link_destroy(void);
24 void bearer_tokens_destroy(void);
25 void alerts_by_x_cleanup(void);
26 +void websocket_threads_join(void);
27
28 static bool abort_on_fatal = true;
29
@@ -294,6 +295,7 @@ static void netdata_cleanup_and_exit(EXIT_REASON reason, bool abnormal, bool exi
295 if (!abnormal)
296 add_agent_event(EVENT_AGENT_SHUTDOWN_TIME, (int64_t)(now_monotonic_usec() - shutdown_start_time));
297
298 + websocket_threads_join();
299 nd_thread_join_threads();
300 sqlite_close_databases();
301 watcher_step_complete(WATCHER_STEP_ID_CLOSE_SQL_DATABASES);
src/daemon/pulse/pulse-workers.c
+1
@@ -153,6 +153,7 @@ static struct worker_utilization all_workers_utilization[] = {
153 { .name = "PROFILER", .family = "workers profile", .priority = 1000000 },
154 { .name = "PGCEVICT", .family = "workers dbengine eviction", .priority = 1000000 },
155 { .name = "BACKFILL", .family = "workers backfill", .priority = 1000000 },
156 + { .name = "WEBSOCKET", .family = "workers websocket", .priority = 1000000 },
157
158 // has to be terminated with a NULL
159 { .name = NULL, .family = NULL }
src/database/contexts/api_v2_contexts_agents.c
+31 -77
@@ -3,20 +3,10 @@
3 #include "api_v2_contexts.h"
4 #include "aclk/aclk_capas.h"
5 #include "database/rrd-metadata.h"
6 +#include "database/rrd-retention.h"
7
8 void build_info_to_json_object(BUFFER *b);
9
9 -static time_t round_retention(time_t retention_seconds) {
10 - if(retention_seconds > 60 * 86400)
11 - retention_seconds = HOWMANY(retention_seconds, 86400) * 86400;
12 - else if(retention_seconds > 86400)
13 - retention_seconds = HOWMANY(retention_seconds, 3600) * 3600;
14 - else
15 - retention_seconds = HOWMANY(retention_seconds, 60) * 60;
16 -
17 - return retention_seconds;
18 -}
19 -
10 void buffer_json_agents_v2(BUFFER *wb, struct query_timings *timings, time_t now_s, bool info, bool array) {
11 if(!now_s)
12 now_s = now_realtime_sec();
@@ -73,6 +63,7 @@ void buffer_json_agents_v2(BUFFER *wb, struct query_timings *timings, time_t now
63 {
64 buffer_json_member_add_uint64(wb, "collected", metadata.contexts.collected);
65 buffer_json_member_add_uint64(wb, "available", metadata.contexts.available);
66 + buffer_json_member_add_uint64(wb, "unique", metadata.contexts.unique);
67 }
68 buffer_json_object_close(wb);
69
@@ -85,77 +76,40 @@ void buffer_json_agents_v2(BUFFER *wb, struct query_timings *timings, time_t now
76 }
77 buffer_json_object_close(wb); // api
78
88 - buffer_json_member_add_array(wb, "db_size");
89 - size_t group_seconds;
90 - for (size_t tier = 0; tier < nd_profile.storage_tiers; tier++) {
91 - STORAGE_ENGINE *eng = localhost->db[tier].eng;
92 - if (!eng) continue;
93 -
94 - group_seconds = get_tier_grouping(tier) * localhost->rrd_update_every;
95 - uint64_t max = storage_engine_disk_space_max(eng->seb, localhost->db[tier].si);
96 - uint64_t used = storage_engine_disk_space_used(eng->seb, localhost->db[tier].si);
97 -#ifdef ENABLE_DBENGINE
98 - if (!max && eng->seb == STORAGE_ENGINE_BACKEND_DBENGINE) {
99 - max = rrdeng_get_directory_free_bytes_space(multidb_ctx[tier]);
100 - max += used;
101 - }
102 -#endif
103 - time_t first_time_s = storage_engine_global_first_time_s(eng->seb, localhost->db[tier].si);
104 -// size_t currently_collected_metrics = storage_engine_collected_metrics(eng->seb, localhost->db[tier].si);
79 + // Get retention information using our new function
80 + RRDSTATS_RETENTION retention = rrdstats_retention_collect();
81
106 - NETDATA_DOUBLE percent;
107 - if (used && max)
108 - percent = (NETDATA_DOUBLE) used * 100.0 / (NETDATA_DOUBLE) max;
109 - else
110 - percent = 0.0;
82 + buffer_json_member_add_array(wb, "db_size");
83 + for (size_t i = 0; i < retention.storage_tiers; i++) {
84 + RRD_STORAGE_TIER *tier_info = &retention.tiers[i];
85 + if (!tier_info->backend || tier_info->tier != i)
86 + continue;
87
88 buffer_json_add_array_item_object(wb);
113 - buffer_json_member_add_uint64(wb, "tier", tier);
114 - char human_duration[128];
115 - duration_snprintf_time_t(human_duration, sizeof(human_duration), (stime_t)group_seconds);
116 - buffer_json_member_add_string(wb, "granularity", human_duration);
117 -
118 - buffer_json_member_add_uint64(wb, "metrics", storage_engine_metrics(eng->seb, localhost->db[tier].si));
119 - buffer_json_member_add_uint64(wb, "samples", storage_engine_samples(eng->seb, localhost->db[tier].si));
120 -
121 - if(used || max) {
122 - buffer_json_member_add_uint64(wb, "disk_used", used);
123 - buffer_json_member_add_uint64(wb, "disk_max", max);
124 - buffer_json_member_add_double(wb, "disk_percent", percent);
89 + buffer_json_member_add_uint64(wb, "tier", tier_info->tier);
90 + buffer_json_member_add_string(wb, "granularity", tier_info->granularity_human);
91 + buffer_json_member_add_uint64(wb, "metrics", tier_info->metrics);
92 + buffer_json_member_add_uint64(wb, "samples", tier_info->samples);
93 +
94 + if(tier_info->disk_used || tier_info->disk_max) {
95 + buffer_json_member_add_uint64(wb, "disk_used", tier_info->disk_used);
96 + buffer_json_member_add_uint64(wb, "disk_max", tier_info->disk_max);
97 + // Format disk_percent to have only 2 decimal places
98 + double rounded_percent = floor(tier_info->disk_percent * 100.0 + 0.5) / 100.0;
99 + buffer_json_member_add_double(wb, "disk_percent", rounded_percent);
100 }
101
127 - if(first_time_s < now_s) {
128 - time_t retention = now_s - first_time_s;
129 -
130 - buffer_json_member_add_time_t(wb, "from", first_time_s);
131 - buffer_json_member_add_time_t(wb, "to", now_s);
132 - buffer_json_member_add_time_t(wb, "retention", retention);
133 -
134 - duration_snprintf(human_duration, sizeof(human_duration),
135 - round_retention(retention), "s", false);
136 -
137 - buffer_json_member_add_string(wb, "retention_human", human_duration);
138 -
139 - if(used || max) { // we have disk space information
140 - time_t time_retention = 0;
141 -#ifdef ENABLE_DBENGINE
142 - time_retention = multidb_ctx[tier]->config.max_retention_s;
143 -#endif
144 - time_t space_retention = (time_t)((NETDATA_DOUBLE)(now_s - first_time_s) * 100.0 / percent);
145 - time_t actual_retention = MIN(space_retention, time_retention ? time_retention : space_retention);
146 -
147 - duration_snprintf(
148 - human_duration, sizeof(human_duration),
149 - (int)time_retention, "s", false);
150 -
151 - buffer_json_member_add_time_t(wb, "requested_retention", time_retention);
152 - buffer_json_member_add_string(wb, "requested_retention_human", human_duration);
153 -
154 - duration_snprintf(human_duration, sizeof(human_duration),
155 - (int)round_retention(actual_retention), "s", false);
156 -
157 - buffer_json_member_add_time_t(wb, "expected_retention", actual_retention);
158 - buffer_json_member_add_string(wb, "expected_retention_human", human_duration);
102 + if(tier_info->first_time_s < tier_info->last_time_s) {
103 + buffer_json_member_add_time_t(wb, "from", tier_info->first_time_s);
104 + buffer_json_member_add_time_t(wb, "to", tier_info->last_time_s);
105 + buffer_json_member_add_time_t(wb, "retention", tier_info->retention);
106 + buffer_json_member_add_string(wb, "retention_human", tier_info->retention_human);
107 +
108 + if(tier_info->disk_used || tier_info->disk_max) {
109 + buffer_json_member_add_time_t(wb, "requested_retention", tier_info->requested_retention);
110 + buffer_json_member_add_string(wb, "requested_retention_human", tier_info->requested_retention_human);
111 + buffer_json_member_add_time_t(wb, "expected_retention", tier_info->expected_retention);
112 + buffer_json_member_add_string(wb, "expected_retention_human", tier_info->expected_retention_human);
113 }
114 }
115 buffer_json_object_close(wb);
src/database/contexts/rrdcontext-context-registry.c new
+245
@@ -0,0 +1,245 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "rrdcontext.h"
4 +#include "rrdcontext-internal.h"
5 +#include "rrdcontext-context-registry.h"
6 +
7 +// The registry - using a raw JudyL array
8 +// Key: STRING pointer
9 +// Value: reference count (size_t)
10 +static Pvoid_t context_registry_judyl = NULL;
11 +
12 +// Spinlock to protect access to the registry
13 +static SPINLOCK context_registry_spinlock = SPINLOCK_INITIALIZER;
14 +
15 +// Clean up the context registry
16 +void rrdcontext_context_registry_destroy(void) {
17 + spinlock_lock(&context_registry_spinlock);
18 +
19 + Word_t index = 0;
20 + Pvoid_t *PValue;
21 +
22 + // Free the strings we've held references to
23 + PValue = JudyLFirst(context_registry_judyl, &index, PJE0);
24 + while (PValue) {
25 + // Each string has been duplicated when added, so free it
26 + string_freez((STRING *)index);
27 + PValue = JudyLNext(context_registry_judyl, &index, PJE0);
28 + }
29 +
30 + // Free the entire Judy array
31 + JudyLFreeArray(&context_registry_judyl, PJE0);
32 +
33 + spinlock_unlock(&context_registry_spinlock);
34 +}
35 +
36 +// Add a context to the registry or increment its reference count
37 +bool rrdcontext_context_registry_add(STRING *context) {
38 + if (unlikely(!context))
39 + return false;
40 +
41 + bool is_new = false;
42 +
43 + spinlock_lock(&context_registry_spinlock);
44 +
45 + // Get or insert a slot for this context
46 + Pvoid_t *PValue = JudyLIns(&context_registry_judyl, (Word_t)context, PJE0);
47 +
48 + if (unlikely(PValue == PJERR)) {
49 + // Memory allocation error
50 + internal_error(true, "RRDCONTEXT: JudyL memory allocation failed in rrdcontext_context_registry_add()");
51 + spinlock_unlock(&context_registry_spinlock);
52 + return false;
53 + }
54 +
55 + size_t count = (size_t)(Word_t)*PValue;
56 +
57 + if (count == 0) {
58 + // This is a new context - duplicate the string to increase its reference count
59 + string_dup(context);
60 + is_new = true;
61 + }
62 +
63 + // Increment the reference count
64 + *PValue = (void *)(Word_t)(count + 1);
65 +
66 + spinlock_unlock(&context_registry_spinlock);
67 +
68 + return is_new;
69 +}
70 +
71 +// Remove a context from the registry or decrement its reference count
72 +bool rrdcontext_context_registry_remove(STRING *context) {
73 + if (unlikely(!context))
74 + return false;
75 +
76 + bool is_last = false;
77 +
78 + spinlock_lock(&context_registry_spinlock);
79 +
80 + // Try to get the value for this context
81 + Pvoid_t *PValue = JudyLGet(context_registry_judyl, (Word_t)context, PJE0);
82 +
83 + if (PValue) {
84 + size_t count = (size_t)(Word_t)*PValue;
85 +
86 + if (count > 1) {
87 + // More than one reference, just decrement
88 + *PValue = (void *)(Word_t)(count - 1);
89 + }
90 + else {
91 + // Last reference - remove it and free the string
92 + int ret;
93 + ret = JudyLDel(&context_registry_judyl, (Word_t)context, PJE0);
94 + if (ret == 1) {
95 + string_freez(context);
96 + is_last = true;
97 + }
98 + }
99 + }
100 +
101 + spinlock_unlock(&context_registry_spinlock);
102 +
103 + return is_last;
104 +}
105 +
106 +// Get the current number of unique contexts
107 +size_t rrdcontext_context_registry_unique_count(void) {
108 + Word_t count = 0;
109 +
110 + spinlock_lock(&context_registry_spinlock);
111 +
112 + // Count entries manually
113 + Word_t index = 0;
114 + Pvoid_t *PValue = JudyLFirst(context_registry_judyl, &index, PJE0);
115 +
116 + while (PValue) {
117 + count++;
118 + PValue = JudyLNext(context_registry_judyl, &index, PJE0);
119 + }
120 +
121 + spinlock_unlock(&context_registry_spinlock);
122 +
123 + return (size_t)count;
124 +}
125 +
126 +void rrdcontext_context_registry_json_mcp_array(BUFFER *wb, SIMPLE_PATTERN *pattern) {
127 + spinlock_lock(&context_registry_spinlock);
128 +
129 + buffer_json_member_add_array(wb, "header");
130 + buffer_json_add_array_item_string(wb, "context");
131 + buffer_json_add_array_item_string(wb, "number_of_nodes_having_it");
132 + buffer_json_array_close(wb);
133 +
134 + buffer_json_member_add_array(wb, "contexts");
135 +
136 + Word_t index = 0;
137 + bool first = true;
138 + Pvoid_t *PValue;
139 + while ((PValue = JudyLFirstThenNext(context_registry_judyl, &index, &first))) {
140 + if (!index || !*PValue) continue;
141 +
142 + const char *context_name = string2str((STRING *)index);
143 +
144 + // Skip if we have a pattern and it doesn't match
145 + if (pattern && !simple_pattern_matches(pattern, context_name))
146 + continue;
147 +
148 + buffer_json_add_array_item_array(wb);
149 + buffer_json_add_array_item_string(wb, context_name);
150 + buffer_json_add_array_item_uint64(wb, *(size_t *)PValue);
151 + buffer_json_array_close(wb);
152 + }
153 +
154 + buffer_json_array_close(wb);
155 +
156 + spinlock_unlock(&context_registry_spinlock);
157 +}
158 +
159 +// Implementation to extract and output unique context categories
160 +void rrdcontext_context_registry_json_mcp_categories_array(BUFFER *wb, SIMPLE_PATTERN *pattern) {
161 + spinlock_lock(&context_registry_spinlock);
162 +
163 + // JudyL array to store unique category STRINGs as keys and counts as values
164 + Pvoid_t categories_judyl = NULL;
165 +
166 + // Header information
167 + buffer_json_member_add_array(wb, "header");
168 + buffer_json_add_array_item_string(wb, "category");
169 + buffer_json_add_array_item_string(wb, "number_of_contexts");
170 + buffer_json_array_close(wb);
171 +
172 + buffer_json_member_add_array(wb, "categories");
173 +
174 + // First pass: count occurrences of each category
175 + Word_t index = 0;
176 + bool first = true;
177 + Pvoid_t *PValue;
178 + while ((PValue = JudyLFirstThenNext(context_registry_judyl, &index, &first))) {
179 + if (!index || !*PValue) continue;
180 +
181 + const char *context_name = string2str((STRING *)index);
182 +
183 + // Find the last dot in the context name
184 + const char *first_dot = strchr(context_name, '.');
185 +
186 + // Create a STRING for the category (everything up to the last dot)
187 + STRING *category_str;
188 + if (first_dot) {
189 + // Create a STRING with the part before the last dot
190 + category_str = string_strndupz(context_name, first_dot - context_name);
191 + } else {
192 + // No dots, use the entire context as the category
193 + category_str = string_strdupz(context_name);
194 + }
195 +
196 + if (!category_str) continue;
197 +
198 + // Get or insert a slot for this category
199 + Pvoid_t *CategoryValue = JudyLIns(&categories_judyl, (Word_t)category_str, PJE0);
200 +
201 + if (CategoryValue) {
202 + // Check if this is a new entry
203 + size_t count = (size_t)(Word_t)*CategoryValue;
204 + if (count > 0) {
205 + // Already exists, free our reference (JudyL already has one)
206 + string_freez(category_str);
207 + }
208 + // Increment the count
209 + *CategoryValue = (void *)(Word_t)(count + 1);
210 + } else {
211 + // Failed to insert, free the STRING
212 + string_freez(category_str);
213 + }
214 + }
215 +
216 + // Second pass: output the unique categories and their counts
217 + index = 0;
218 + first = true;
219 + while ((PValue = JudyLFirstThenNext(categories_judyl, &index, &first))) {
220 + if (!index) continue;
221 +
222 + STRING *category_str = (STRING *)index;
223 + const char *category = string2str(category_str);
224 +
225 + // Apply pattern filtering here, on the category itself
226 + if (!pattern || simple_pattern_matches(pattern, category)) {
227 + size_t count = (size_t)(Word_t)*PValue;
228 +
229 + buffer_json_add_array_item_array(wb);
230 + buffer_json_add_array_item_string(wb, category);
231 + buffer_json_add_array_item_uint64(wb, count);
232 + buffer_json_array_close(wb);
233 + }
234 +
235 + // Free the STRING object as we go
236 + string_freez(category_str);
237 + }
238 +
239 + buffer_json_array_close(wb);
240 +
241 + // Free the JudyL array (values were already freed in the loop above)
242 + JudyLFreeArray(&categories_judyl, PJE0);
243 +
244 + spinlock_unlock(&context_registry_spinlock);
245 +}
src/database/contexts/rrdcontext-context-registry.h new
+20
@@ -0,0 +1,20 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_RRDCONTEXT_CONTEXT_REGISTRY_H
4 +#define NETDATA_RRDCONTEXT_CONTEXT_REGISTRY_H
5 +
6 +#include "libnetdata/libnetdata.h"
7 +
8 +void rrdcontext_context_registry_destroy(void);
9 +
10 +bool rrdcontext_context_registry_add(STRING *context);
11 +bool rrdcontext_context_registry_remove(STRING *context);
12 +
13 +size_t rrdcontext_context_registry_unique_count(void);
14 +
15 +void rrdcontext_context_registry_json_mcp_array(BUFFER *wb, SIMPLE_PATTERN *pattern);
16 +
17 +// Helper function to get context categories (extracts prefixes before the last dot)
18 +void rrdcontext_context_registry_json_mcp_categories_array(BUFFER *wb, SIMPLE_PATTERN *pattern);
19 +
20 +#endif // NETDATA_RRDCONTEXT_CONTEXT_REGISTRY_H
\ No newline at end of file
src/database/contexts/rrdcontext-context.c
+6
@@ -28,6 +28,9 @@ static void rrdcontext_insert_callback(const DICTIONARY_ITEM *item __maybe_unuse
28
29 rc->rrdhost = host;
30 rc->flags = rc->flags & RRD_FLAGS_ALLOWED_EXTERNALLY_ON_NEW_OBJECTS; // no need for atomics at constructor
31 +
32 + // Add the context to the registry to track unique contexts
33 + rrdcontext_context_registry_add(rc->id);
34
35 if(rc->hub.version) {
36 // we are loading data from the SQL database
@@ -95,6 +98,9 @@ static void rrdcontext_delete_callback(const DICTIONARY_ITEM *item __maybe_unuse
98
99 // update the count of contexts
100 __atomic_sub_fetch(&rc->rrdhost->rrdctx.contexts_count, 1, __ATOMIC_RELAXED);
101 +
102 + // Remove the context from the registry
103 + rrdcontext_context_registry_remove(rc->id);
104
105 rrdcontext_del_from_hub_queue(rc, false);
106 rrdcontext_del_from_pp_queue(rc, false);
src/database/contexts/rrdcontext-internal.h
+1
@@ -4,6 +4,7 @@
4 #define NETDATA_RRDCONTEXT_INTERNAL_H 1
5
6 #include "rrdcontext.h"
7 +#include "rrdcontext-context-registry.h"
8 #include "../sqlite/sqlite_context.h"
9 #include "../../aclk/schema-wrappers/rrdcontext-context.h"
10 #include "../../aclk/aclk_contexts_api.h"
src/database/contexts/rrdcontext.h
+2
@@ -760,5 +760,7 @@ static inline bool query_target_has_percentage_units(QUERY_TARGET *qt) {
760 uint32_t rrdcontext_queue_version(RRDCONTEXT_QUEUE_JudyLSet *queue);
761 int32_t rrdcontext_queue_entries(RRDCONTEXT_QUEUE_JudyLSet *queue);
762
763 +#include "rrdcontext-context-registry.h"
764 +
765 #endif // NETDATA_RRDCONTEXT_H
766
src/database/rrd-metadata.c
+5 -1
@@ -3,6 +3,7 @@
3 #define RRDHOST_INTERNALS
4 #include "rrd.h"
5 #include "rrd-metadata.h"
6 +#include "contexts/rrdcontext-context-registry.h"
7
8 // Collect metrics metadata from all hosts
9 RRDSTATS_METADATA rrdstats_metadata_collect(void) {
@@ -10,7 +11,7 @@ RRDSTATS_METADATA rrdstats_metadata_collect(void) {
11 .nodes = { .total = 0, .receiving = 0, .sending = 0, .archived = 0 },
12 .metrics = { .collected = 0, .available = 0 },
13 .instances = { .collected = 0, .available = 0 },
13 - .contexts = { .collected = 0, .available = 0 }
14 + .contexts = { .collected = 0, .available = 0, .unique = 0 }
15 };
16
17 rrd_rdlock();
@@ -45,6 +46,9 @@ RRDSTATS_METADATA rrdstats_metadata_collect(void) {
46 dfe_done(host);
47
48 rrd_rdunlock();
49 +
50 + // Get the count of unique contexts from our registry
51 + metadata.contexts.unique = rrdcontext_context_registry_unique_count();
52
53 return metadata;
54 }
\ No newline at end of file
src/database/rrd-metadata.h
+1
@@ -27,6 +27,7 @@ typedef struct rrdstats_metadata {
27 struct {
28 size_t collected;
29 size_t available;
30 + size_t unique; // Count of unique contexts across all hosts
31 } contexts;
32 } RRDSTATS_METADATA;
33
src/database/rrd-retention.c new
+125
@@ -0,0 +1,125 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#define RRDHOST_INTERNALS
4 +#include "rrd.h"
5 +#include "rrd-retention.h"
6 +#include "libnetdata/parsers/duration.h"
7 +
8 +// Round retention time to more human-readable values (days/hours/minutes)
9 +static time_t round_retention(time_t retention_seconds) {
10 + if(retention_seconds > 60 * 86400)
11 + retention_seconds = HOWMANY(retention_seconds, 86400) * 86400;
12 + else if(retention_seconds > 86400)
13 + retention_seconds = HOWMANY(retention_seconds, 3600) * 3600;
14 + else
15 + retention_seconds = HOWMANY(retention_seconds, 60) * 60;
16 +
17 + return retention_seconds;
18 +}
19 +
20 +// Collect retention statistics from all tiers
21 +RRDSTATS_RETENTION rrdstats_retention_collect(void) {
22 + time_t now_s = now_realtime_sec();
23 +
24 + // Initialize the retention structure
25 + RRDSTATS_RETENTION retention = {
26 + .storage_tiers = 0
27 + };
28 +
29 + rrd_rdlock();
30 +
31 + if(!localhost) {
32 + rrd_rdunlock();
33 + return retention;
34 + }
35 +
36 + // Count the available storage tiers
37 + retention.storage_tiers = nd_profile.storage_tiers;
38 +
39 + // Iterate through all available storage tiers
40 + for(size_t tier = 0; tier < retention.storage_tiers && tier < RRD_MAX_STORAGE_TIERS; tier++) {
41 + STORAGE_ENGINE *eng = localhost->db[tier].eng;
42 + if(!eng)
43 + continue;
44 +
45 + RRD_STORAGE_TIER *tier_info = &retention.tiers[tier];
46 + tier_info->tier = tier;
47 + tier_info->backend = eng->seb;
48 + tier_info->group_seconds = get_tier_grouping(tier) * localhost->rrd_update_every;
49 +
50 + // Format human-readable granularity
51 + duration_snprintf_time_t(tier_info->granularity_human, sizeof(tier_info->granularity_human), (time_t)tier_info->group_seconds);
52 +
53 + // Get metrics and samples counts
54 + tier_info->metrics = storage_engine_metrics(eng->seb, localhost->db[tier].si);
55 + tier_info->samples = storage_engine_samples(eng->seb, localhost->db[tier].si);
56 +
57 + // Get disk usage information
58 + tier_info->disk_max = storage_engine_disk_space_max(eng->seb, localhost->db[tier].si);
59 + tier_info->disk_used = storage_engine_disk_space_used(eng->seb, localhost->db[tier].si);
60 +
61 +#ifdef ENABLE_DBENGINE
62 + if(!tier_info->disk_max && eng->seb == STORAGE_ENGINE_BACKEND_DBENGINE) {
63 + tier_info->disk_max = rrdeng_get_directory_free_bytes_space(multidb_ctx[tier]);
64 + tier_info->disk_max += tier_info->disk_used;
65 + }
66 +#endif
67 +
68 + // Calculate disk usage percentage
69 + if(tier_info->disk_used && tier_info->disk_max)
70 + tier_info->disk_percent = (double)tier_info->disk_used * 100.0 / (double)tier_info->disk_max;
71 + else
72 + tier_info->disk_percent = 0.0;
73 +
74 + // Get retention information
75 + tier_info->first_time_s = storage_engine_global_first_time_s(eng->seb, localhost->db[tier].si);
76 + tier_info->last_time_s = now_s;
77 +
78 + if(tier_info->first_time_s < tier_info->last_time_s) {
79 + tier_info->retention = tier_info->last_time_s - tier_info->first_time_s;
80 +
81 + // Format human-readable retention
82 + duration_snprintf(tier_info->retention_human, sizeof(tier_info->retention_human),
83 + round_retention(tier_info->retention), "s", false);
84 +
85 + if(tier_info->disk_used || tier_info->disk_max) {
86 + // Get requested retention time
87 + tier_info->requested_retention = 0;
88 +#ifdef ENABLE_DBENGINE
89 + if(eng->seb == STORAGE_ENGINE_BACKEND_DBENGINE)
90 + tier_info->requested_retention = multidb_ctx[tier]->config.max_retention_s;
91 +#endif
92 +
93 + // Format human-readable requested retention
94 + duration_snprintf(tier_info->requested_retention_human, sizeof(tier_info->requested_retention_human),
95 + (int)tier_info->requested_retention, "s", false);
96 +
97 + // Calculate expected retention based on current usage
98 + time_t space_retention = 0;
99 + if(tier_info->disk_percent > 0)
100 + space_retention = (time_t)((double)(now_s - tier_info->first_time_s) * 100.0 / tier_info->disk_percent);
101 +
102 + tier_info->expected_retention = (tier_info->requested_retention && tier_info->requested_retention < space_retention)
103 + ? tier_info->requested_retention
104 + : space_retention;
105 +
106 + // Format human-readable expected retention
107 + duration_snprintf(tier_info->expected_retention_human, sizeof(tier_info->expected_retention_human),
108 + (int)round_retention(tier_info->expected_retention), "s", false);
109 + }
110 + }
111 + else {
112 + // No data yet in this tier
113 + tier_info->retention = 0;
114 + tier_info->retention_human[0] = '\0';
115 + tier_info->requested_retention = 0;
116 + tier_info->requested_retention_human[0] = '\0';
117 + tier_info->expected_retention = 0;
118 + tier_info->expected_retention_human[0] = '\0';
119 + }
120 + }
121 +
122 + rrd_rdunlock();
123 +
124 + return retention;
125 +}
\ No newline at end of file
src/database/rrd-retention.h new
+47
@@ -0,0 +1,47 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_RRD_RETENTION_H
4 +#define NETDATA_RRD_RETENTION_H
5 +
6 +#include "libnetdata/libnetdata.h"
7 +#include "storage-engine.h"
8 +
9 +// Maximum number of storage tiers the system supports
10 +#define RRD_MAX_STORAGE_TIERS 32
11 +
12 +// Structure to hold information about each storage tier
13 +typedef struct rrd_storage_tier {
14 + size_t tier; // Tier number
15 + STORAGE_ENGINE_BACKEND backend; // Storage engine backend (RRDDIM or DBENGINE)
16 + size_t group_seconds; // Granularity in seconds
17 + char granularity_human[32]; // Human-readable granularity string
18 +
19 + size_t metrics; // Number of metrics in this tier
20 + size_t samples; // Number of samples in this tier
21 +
22 + uint64_t disk_used; // Disk space used in bytes
23 + uint64_t disk_max; // Maximum available disk space in bytes
24 + double disk_percent; // Disk usage percentage (0.0-100.0)
25 +
26 + time_t first_time_s; // Oldest timestamp in this tier
27 + time_t last_time_s; // Most recent timestamp in this tier
28 + time_t retention; // Current retention in seconds (last_time_s - first_time_s)
29 + char retention_human[32]; // Human-readable current retention
30 +
31 + time_t requested_retention; // Configured maximum retention in seconds
32 + char requested_retention_human[32]; // Human-readable configured retention
33 +
34 + time_t expected_retention; // Expected retention based on current usage
35 + char expected_retention_human[32]; // Human-readable expected retention
36 +} RRD_STORAGE_TIER;
37 +
38 +// Main structure to hold retention information across all tiers
39 +typedef struct rrdstats_retention {
40 + size_t storage_tiers; // Number of available storage tiers
41 + RRD_STORAGE_TIER tiers[RRD_MAX_STORAGE_TIERS]; // Array of tier information
42 +} RRDSTATS_RETENTION;
43 +
44 +// Function to collect retention statistics
45 +RRDSTATS_RETENTION rrdstats_retention_collect(void);
46 +
47 +#endif // NETDATA_RRD_RETENTION_H
\ No newline at end of file
src/libnetdata/circular_buffer/circular_buffer.c
+129 -9
@@ -1,7 +1,10 @@
1 #include "../libnetdata.h"
2
3 -struct circular_buffer *cbuffer_new(size_t initial, size_t max, size_t *statistics) {
4 - struct circular_buffer *buf = mallocz(sizeof(struct circular_buffer));
3 +// Initialize a pre-allocated circular buffer
4 +void cbuffer_init(struct circular_buffer *buf, size_t initial, size_t max, size_t *statistics) {
5 + if (unlikely(!buf))
6 + return;
7 +
8 buf->size = initial;
9 buf->data = mallocz(initial);
10 buf->write = 0;
@@ -10,19 +13,44 @@ struct circular_buffer *cbuffer_new(size_t initial, size_t max, size_t *statisti
13 buf->statistics = statistics;
14
15 if(buf->statistics)
13 - __atomic_add_fetch(buf->statistics, sizeof(struct circular_buffer) + buf->size, __ATOMIC_RELAXED);
16 + __atomic_add_fetch(buf->statistics, buf->size, __ATOMIC_RELAXED);
17 +}
18 +
19 +// Cleanup resources for a pre-allocated circular buffer
20 +void cbuffer_cleanup(struct circular_buffer *buf) {
21 + if (unlikely(!buf))
22 + return;
23 +
24 + if(buf->statistics)
25 + __atomic_sub_fetch(buf->statistics, buf->size, __ATOMIC_RELAXED);
26 +
27 + freez(buf->data);
28 + buf->data = NULL;
29 + buf->size = 0;
30 + buf->write = 0;
31 + buf->read = 0;
32 +}
33 +
34 +// Allocate and initialize a new circular buffer
35 +struct circular_buffer *cbuffer_new(size_t initial, size_t max, size_t *statistics) {
36 + struct circular_buffer *buf = mallocz(sizeof(struct circular_buffer));
37 + cbuffer_init(buf, initial, max, statistics);
38 +
39 + if(buf->statistics)
40 + __atomic_add_fetch(buf->statistics, sizeof(struct circular_buffer), __ATOMIC_RELAXED);
41
42 return buf;
43 }
44
45 +// Free a circular buffer allocated with cbuffer_new
46 void cbuffer_free(struct circular_buffer *buf) {
47 if (unlikely(!buf))
48 return;
49
50 if(buf->statistics)
23 - __atomic_sub_fetch(buf->statistics, sizeof(struct circular_buffer) + buf->size, __ATOMIC_RELAXED);
51 + __atomic_sub_fetch(buf->statistics, sizeof(struct circular_buffer), __ATOMIC_RELAXED);
52
25 - freez(buf->data);
53 + cbuffer_cleanup(buf);
54 freez(buf);
55 }
56
@@ -63,13 +91,18 @@ static int cbuffer_realloc_unsafe(struct circular_buffer *buf) {
91 return 0;
92 }
93
94 +ALWAYS_INLINE
95 +size_t cbuffer_used_size_unsafe(struct circular_buffer *buf) {
96 + return (buf->write >= buf->read) ? (buf->write - buf->read) : (buf->size - buf->read + buf->write);
97 +}
98 +
99 +ALWAYS_INLINE
100 size_t cbuffer_available_size_unsafe(struct circular_buffer *buf) {
67 - size_t len = (buf->write >= buf->read) ? (buf->write - buf->read) : (buf->size - buf->read + buf->write);
68 - return buf->max_size - len;
101 + return buf->max_size - cbuffer_used_size_unsafe(buf);
102 }
103
104 int cbuffer_add_unsafe(struct circular_buffer *buf, const char *d, size_t d_len) {
72 - size_t len = (buf->write >= buf->read) ? (buf->write - buf->read) : (buf->size - buf->read + buf->write);
105 + size_t len = cbuffer_used_size_unsafe(buf);
106 while (d_len + len >= buf->size) {
107 if (cbuffer_realloc_unsafe(buf)) {
108 return 1;
@@ -90,6 +123,7 @@ int cbuffer_add_unsafe(struct circular_buffer *buf, const char *d, size_t d_len)
123 }
124
125 // Assume caller does not remove too many bytes (i.e. read will jump over write)
126 +ALWAYS_INLINE
127 void cbuffer_remove_unsafe(struct circular_buffer *buf, size_t num) {
128 buf->read += num;
129 // Assume num < size (i.e. caller cannot remove more bytes than are in the buffer)
@@ -97,6 +131,7 @@ void cbuffer_remove_unsafe(struct circular_buffer *buf, size_t num) {
131 buf->read -= buf->size;
132 }
133
134 +ALWAYS_INLINE
135 size_t cbuffer_next_unsafe(struct circular_buffer *buf, char **start) {
136 if (start != NULL)
137 *start = buf->data + buf->read;
@@ -107,7 +142,92 @@ size_t cbuffer_next_unsafe(struct circular_buffer *buf, char **start) {
142 return buf->size - buf->read;
143 }
144
145 +ALWAYS_INLINE
146 void cbuffer_flush(struct circular_buffer*buf) {
147 buf->write = 0;
148 buf->read = 0;
113 -}
\ No newline at end of file
149 +}
150 +
151 +// Ensures that the requested size is available as a contiguous block in the buffer
152 +// Returns true if there's enough data and it's now contiguous, false otherwise
153 +bool cbuffer_ensure_unwrapped_size(struct circular_buffer *buf, size_t size) {
154 + if (unlikely(!buf || !buf->data))
155 + return false;
156 +
157 + size_t used = cbuffer_used_size_unsafe(buf);
158 + if(used < size)
159 + return false;
160 +
161 + char *unwrapped;
162 + size_t unwrapped_size = cbuffer_next_unsafe(buf, &unwrapped);
163 + if(unwrapped_size >= size)
164 + return true;
165 +
166 + size_t wrapped_size = used - unwrapped_size;
167 +
168 + char *tmp = mallocz(unwrapped_size);
169 + memcpy(tmp, unwrapped, unwrapped_size);
170 + cbuffer_remove_unsafe(buf, unwrapped_size);
171 +
172 + memmove(buf->data + unwrapped_size, buf->data, wrapped_size);
173 + memcpy(buf->data, tmp, unwrapped_size);
174 + freez(tmp);
175 +
176 + buf->read = 0;
177 + buf->write = unwrapped_size + wrapped_size;
178 +
179 + return true;
180 +}
181 +
182 +// Reserve space in the circular buffer for direct writing
183 +// Returns a pointer to the reserved space, or NULL if reservation fails
184 +char *cbuffer_reserve_unsafe(struct circular_buffer *buf, size_t size) {
185 + if (unlikely(!buf || !buf->data || size == 0))
186 + return NULL;
187 +
188 + // First, make sure we have enough space in the buffer
189 + size_t len = cbuffer_used_size_unsafe(buf);
190 + while (size + len >= buf->size) {
191 + if (cbuffer_realloc_unsafe(buf)) {
192 + // Can't grow buffer anymore
193 + return NULL;
194 + }
195 + }
196 +
197 + if(buf->write + size > buf->size) {
198 + if (!cbuffer_ensure_unwrapped_size(buf, len))
199 + return NULL;
200 +
201 + if(buf->read != 0 && buf->write + size > buf->size) {
202 + // It is a contiguous buffer, but we need to move the data
203 + // Move the data to the beginning of the buffer
204 + memmove(buf->data, buf->data + buf->read, buf->write - buf->read);
205 + buf->write -= buf->read;
206 + buf->read = 0;
207 + }
208 + }
209 +
210 + // Check if we can write contiguously from the current write position
211 + if (buf->write + size <= buf->size) {
212 + // Simple case - we have enough space at the current write position
213 + return buf->data + buf->write;
214 + }
215 + else {
216 + // impossible case since cbuffer_ensure_unwrapped_size() returned true
217 + return NULL;
218 + }
219 +}
220 +
221 +// Commit the reserved space after writing to it
222 +// Size should be less than or equal to the size passed to cbuffer_reserve_unsafe
223 +void cbuffer_commit_reserved_unsafe(struct circular_buffer *buf, size_t size) {
224 + if (unlikely(!buf || !buf->data || size == 0))
225 + return;
226 +
227 + // Update the write pointer
228 + buf->write += size;
229 +
230 + // Handle wrap-around if we've gone past the buffer boundary
231 + if (buf->write >= buf->size)
232 + buf->write -= buf->size;
233 +}
src/libnetdata/circular_buffer/circular_buffer.h
+17 -1
@@ -9,12 +9,28 @@ struct circular_buffer {
9 char *data;
10 };
11
12 +// Allocation/deallocation functions
13 struct circular_buffer *cbuffer_new(size_t initial, size_t max, size_t *statistics);
14 void cbuffer_free(struct circular_buffer *buf);
15 +
16 +// Static allocation support
17 +void cbuffer_init(struct circular_buffer *buf, size_t initial, size_t max, size_t *statistics);
18 +void cbuffer_cleanup(struct circular_buffer *buf);
19 +
20 +// Buffer operations
21 int cbuffer_add_unsafe(struct circular_buffer *buf, const char *d, size_t d_len);
22 void cbuffer_remove_unsafe(struct circular_buffer *buf, size_t num);
23 size_t cbuffer_next_unsafe(struct circular_buffer *buf, char **start);
24 size_t cbuffer_available_size_unsafe(struct circular_buffer *buf);
18 -void cbuffer_flush(struct circular_buffer*buf);
25 +void cbuffer_flush(struct circular_buffer *buf);
26 +
27 +// Reserve/commit operations for direct buffer access
28 +char *cbuffer_reserve_unsafe(struct circular_buffer *buf, size_t size);
29 +void cbuffer_commit_reserved_unsafe(struct circular_buffer *buf, size_t size);
30 +
31 +// Check if a size is wrapped in buffer and unwrap if necessary
32 +bool cbuffer_ensure_unwrapped_size(struct circular_buffer *buf, size_t size);
33 +
34 +size_t cbuffer_used_size_unsafe(struct circular_buffer *buf);
35
36 #endif
src/libnetdata/http/http_defs.h
+2
@@ -9,6 +9,7 @@
9
10 // HTTP_CODES 1XX
11 #define HTTP_RESP_SWITCH_PROTO 101
12 +#define HTTP_RESP_WEBSOCKET_HANDSHAKE 101 // WebSocket uses 101 Switching Protocols
13
14 // HTTP_CODES 2XX Success
15 #define HTTP_RESP_OK 200
@@ -51,6 +52,7 @@ typedef enum __attribute__((__packed__)) {
52 HTTP_REQUEST_MODE_FILECOPY = 5,
53 HTTP_REQUEST_MODE_OPTIONS = 6,
54 HTTP_REQUEST_MODE_STREAM = 7,
55 + HTTP_REQUEST_MODE_WEBSOCKET = 8,
56 } HTTP_REQUEST_MODE;
57
58 ENUM_STR_DEFINE_FUNCTIONS_EXTERN(HTTP_REQUEST_MODE);
src/libnetdata/log/nd_log.h
+28 -36
@@ -80,42 +80,34 @@ struct log_stack_entry {
80 void log_stack_pop(void *ptr);
81 void log_stack_push(struct log_stack_entry *lgs);
82
83 -#define D_WEB_BUFFER 0x0000000000000001
84 -#define D_WEB_CLIENT 0x0000000000000002
85 -#define D_LISTENER 0x0000000000000004
86 -#define D_WEB_DATA 0x0000000000000008
87 -#define D_OPTIONS 0x0000000000000010
88 -#define D_PROCNETDEV_LOOP 0x0000000000000020
89 -#define D_RRD_STATS 0x0000000000000040
90 -#define D_WEB_CLIENT_ACCESS 0x0000000000000080
91 -#define D_TC_LOOP 0x0000000000000100
92 -#define D_DEFLATE 0x0000000000000200
93 -#define D_CONFIG 0x0000000000000400
94 -#define D_PLUGINSD 0x0000000000000800
95 -#define D_CHILDS 0x0000000000001000
96 -#define D_EXIT 0x0000000000002000
97 -#define D_CHECKS 0x0000000000004000
98 -#define D_NFACCT_LOOP 0x0000000000008000
99 -#define D_PROCFILE 0x0000000000010000
100 -#define D_RRD_CALLS 0x0000000000020000
101 -#define D_DICTIONARY 0x0000000000040000
102 -#define D_MEMORY 0x0000000000080000
103 -#define D_CGROUP 0x0000000000100000
104 -#define D_REGISTRY 0x0000000000200000
105 -#define D_VARIABLES 0x0000000000400000
106 -#define D_HEALTH 0x0000000000800000
107 -#define D_CONNECT_TO 0x0000000001000000
108 -#define D_RRDHOST 0x0000000002000000
109 -#define D_LOCKS 0x0000000004000000
110 -#define D_EXPORTING 0x0000000008000000
111 -#define D_STATSD 0x0000000010000000
112 -#define D_POLLFD 0x0000000020000000
113 -#define D_STREAM 0x0000000040000000
114 -#define D_ANALYTICS 0x0000000080000000
115 -#define D_RRDENGINE 0x0000000100000000
116 -#define D_ACLK 0x0000000200000000
117 -#define D_REPLICATION 0x0000002000000000
118 -#define D_SYSTEM 0x8000000000000000
83 +#define D_WEB_BUFFER (1ULL << 0)
84 +#define D_WEB_CLIENT (1ULL << 1)
85 +#define D_LISTENER (1ULL << 2)
86 +#define D_WEB_DATA (1ULL << 3)
87 +#define D_OPTIONS (1ULL << 4)
88 +#define D_PROCNETDEV_LOOP (1ULL << 5)
89 +#define D_RRD_STATS (1ULL << 6)
90 +#define D_WEB_CLIENT_ACCESS (1ULL << 7)
91 +#define D_TC_LOOP (1ULL << 8)
92 +#define D_DEFLATE (1ULL << 9)
93 +#define D_CONFIG (1ULL << 10)
94 +#define D_PLUGINSD (1ULL << 11)
95 +#define D_PROCFILE (1ULL << 12)
96 +#define D_RRD_CALLS (1ULL << 13)
97 +#define D_DICTIONARY (1ULL << 14)
98 +#define D_CGROUP (1ULL << 15)
99 +#define D_REGISTRY (1ULL << 16)
100 +#define D_HEALTH (1ULL << 17)
101 +#define D_LOCKS (1ULL << 18)
102 +#define D_EXPORTING (1ULL << 19)
103 +#define D_STATSD (1ULL << 20)
104 +#define D_STREAM (1ULL << 21)
105 +#define D_ANALYTICS (1ULL << 22)
106 +#define D_RRDENGINE (1ULL << 23)
107 +#define D_ACLK (1ULL << 24)
108 +#define D_WEBSOCKET (1ULL << 25)
109 +#define D_MCP (1ULL << 26)
110 +#define D_SYSTEM (1ULL << 27)
111
112 extern uint64_t debug_flags;
113 extern const char *program_name;
src/libnetdata/socket/nd-poll.c
+1 -1
@@ -6,7 +6,7 @@
6 #define POLLRDHUP 0
7 #endif
8
9 -#if defined(OS_LINUX_DISABLE_EPOLL_DUE_TO_BUG)
9 +#if defined(OS_LINUX_DISABLED_DUE_TO_BUG_IN_KERNEL_EPOLL)
10 #include <sys/epoll.h>
11
12 struct fd_info {
src/libnetdata/socket/poll-events.c
+35 -7
@@ -1,9 +1,10 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 +#include "daemon/static_threads.h"
4 #include "libnetdata/libnetdata.h"
5
6 static inline void poll_process_updated_events(POLLINFO *pi) {
6 - if(pi->events != pi->events_we_wait_for) {
7 + if(pi->events != pi->events_we_wait_for && !(pi->flags & POLLINFO_FLAG_REMOVED_FROM_POLL)) {
8 if(!nd_poll_upd(pi->p->ndpl, pi->fd, pi->events))
9 nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to update socket %d to nd_poll", pi->fd);
10 pi->events_we_wait_for = pi->events;
@@ -72,15 +73,26 @@ POLLINFO *poll_add_fd(POLLJOB *p
73 return pi;
74 }
75
76 +void poll_process_remove_from_poll(POLLINFO *pi) {
77 + POLLJOB *p = pi->p;
78 +
79 + if(!nd_poll_del(p->ndpl, pi->fd))
80 + nd_log(NDLS_DAEMON, NDLP_ERR,
81 + "Failed to delete socket %d from nd_poll() - is the socket already closed?", pi->fd);
82 + else
83 + pi->flags |= POLLINFO_FLAG_REMOVED_FROM_POLL;
84 +}
85 +
86 static inline void poll_close_fd(POLLINFO *pi, const char *func) {
87 POLLJOB *p = pi->p;
88
89 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(p->ll, pi, prev, next);
79 - if(!nd_poll_del(p->ndpl, pi->fd))
80 - // this is ok, if the socket is already closed
81 - nd_log(NDLS_DAEMON, NDLP_DEBUG,
90 + if(!(pi->flags & POLLINFO_FLAG_REMOVED_FROM_POLL) && !nd_poll_del(p->ndpl, pi->fd))
91 + nd_log(NDLS_DAEMON, NDLP_ERR,
92 "Failed to delete socket %d from nd_poll() - called from %s() - is the socket already closed?",
93 pi->fd, func);
94 + else
95 + pi->flags |= POLLINFO_FLAG_REMOVED_FROM_POLL;
96
97 if(pi->flags & POLLINFO_FLAG_CLIENT_SOCKET) {
98 pi->del_callback(pi);
@@ -394,6 +406,14 @@ void poll_events(LISTEN_SOCKETS *sockets
406
407 CLEANUP_FUNCTION_REGISTER(poll_events_cleanup) cleanup_ptr = &p;
408
409 + size_t iteration_counter = 0,
410 + timeout_counter = 0,
411 + errors_counter = 0,
412 + read_counter = 0,
413 + writes_counter = 0,
414 + unhandled_counter = 0,
415 + cleanup_counter = 0;
416 +
417 while(!check_to_stop_callback() && !nd_thread_signaled_to_cancel()) {
418 if(unlikely(timer_usec)) {
419 now_usec = now_boottime_usec();
@@ -424,6 +444,7 @@ void poll_events(LISTEN_SOCKETS *sockets
444
445 nd_poll_result_t result;
446 retval = nd_poll_wait(p.ndpl, ND_CHECK_CANCELLABILITY_WHILE_WAITING_EVERY_MS, &result);
447 + iteration_counter++;
448 time_t now = now_boottime_sec();
449
450 if(unlikely(retval == -1)) {
@@ -431,20 +452,23 @@ void poll_events(LISTEN_SOCKETS *sockets
452 break;
453 }
454 else if(unlikely(!retval)) {
455 + timeout_counter++;
456 // timeout
457 ;
458 }
459 else {
460 POLLINFO *pi = (POLLINFO *)result.data;
461
440 - if(result.events & (ND_POLL_HUP | ND_POLL_INVALID | ND_POLL_ERROR))
462 + if(result.events & (ND_POLL_HUP | ND_POLL_INVALID | ND_POLL_ERROR)) {
463 + errors_counter++;
464 poll_process_error(pi, result.events);
442 -
465 + }
466 else if(result.events & ND_POLL_WRITE) {
467 + writes_counter++;
468 poll_process_send(pi, now);
469 }
446 -
470 else if(result.events & ND_POLL_READ) {
471 + read_counter++;
472 if (pi->flags & POLLINFO_FLAG_CLIENT_SOCKET) {
473 if (pi->socktype == SOCK_DGRAM)
474 poll_process_udp_read(pi, now);
@@ -496,6 +520,8 @@ void poll_events(LISTEN_SOCKETS *sockets
520 }
521 }
522 else {
523 + unhandled_counter++;
524 +
525 nd_log(NDLS_DAEMON, NDLP_ERR,
526 "POLLFD: LISTENER: socket slot %zu (fd %d) client %s port %s unhandled event id %d."
527 , i
@@ -510,6 +536,8 @@ void poll_events(LISTEN_SOCKETS *sockets
536 }
537
538 if(unlikely(p.checks_every > 0 && now - last_check > p.checks_every)) {
539 + cleanup_counter++;
540 +
541 last_check = now;
542
543 // cleanup old sockets
src/libnetdata/socket/poll-events.h
+6 -3
@@ -5,9 +5,10 @@
5
6 #include "nd-poll.h"
7
8 -#define POLLINFO_FLAG_SERVER_SOCKET 0x00000001
9 -#define POLLINFO_FLAG_CLIENT_SOCKET 0x00000002
10 -#define POLLINFO_FLAG_DONT_CLOSE 0x00000004
8 +#define POLLINFO_FLAG_SERVER_SOCKET (1U << 0)
9 +#define POLLINFO_FLAG_CLIENT_SOCKET (1U << 1)
10 +#define POLLINFO_FLAG_DONT_CLOSE (1U << 2)
11 +#define POLLINFO_FLAG_REMOVED_FROM_POLL (1U << 3)
12
13 typedef struct poll POLLJOB;
14 typedef struct pollinfo POLLINFO;
@@ -80,6 +81,8 @@ int poll_default_rcv_callback(POLLINFO *pi, nd_poll_event_t *events);
81 void poll_default_del_callback(POLLINFO *pi);
82 void *poll_default_add_callback(POLLINFO *pi, nd_poll_event_t *events, void *data);
83
84 +void poll_process_remove_from_poll(POLLINFO *pi);
85 +
86 POLLINFO *poll_add_fd(POLLJOB *p
87 , int fd
88 , int socktype
src/libnetdata/url/url.h
-4
@@ -19,10 +19,6 @@ char to_hex(char code);
19 /* IMPORTANT: be sure to free() the returned string after use */
20 char *url_encode(const char *str);
21
22 -/* Returns a url-decoded version of str */
23 -/* IMPORTANT: be sure to free() the returned string after use */
24 -char *url_decode(char *str);
25 -
22 char *url_decode_r(char *to, const char *url, size_t size);
23
24 bool url_is_request_complete_and_extract_payload(const char *begin, const char *end, size_t length, BUFFER **post_payload);
src/streaming/stream-receiver-connection.c
+3
@@ -137,6 +137,7 @@ static int stream_receiver_response_too_busy_now(struct web_client *w) {
137 }
138
139 static void stream_receiver_takeover_web_connection(struct web_client *w, struct receiver_state *rpt) {
140 + // Set the file descriptor and ssl from the web client
141 rpt->sock.fd = w->fd;
142 rpt->sock.ssl = w->ssl;
143
@@ -151,6 +152,8 @@ static void stream_receiver_takeover_web_connection(struct web_client *w, struct
152 w->fd = -1;
153
154 buffer_flush(w->response.data);
155 +
156 + web_server_remove_current_socket_from_poll();
157 }
158
159 static void stream_send_error_on_taken_over_connection(struct receiver_state *rpt, const char *msg) {
src/streaming/stream-sender.c
+2
@@ -448,6 +448,8 @@ static void stream_sender_move_running_to_connector_or_remove_internal(struct st
448
449 stream_sender_log_disconnection(sth, s, reason, receiver_reason);
450
451 + // IMPORTANT: make sure it REMOVED from nd_poll() before closing the socket
452 + // otherwise, undefined things will happen due to socket reuse and epoll()
453 nd_sock_close(&s->sock);
454
455 stream_parent_set_host_disconnect_reason(s->host, reason, now_realtime_sec());
src/web/api/http_header.c
+135
@@ -62,6 +62,10 @@ static void http_header_origin(struct web_client *w, const char *v, size_t len _
62 static void http_header_connection(struct web_client *w, const char *v, size_t len __maybe_unused) {
63 if(strcasestr(v, "keep-alive"))
64 web_client_enable_keepalive(w);
65 +
66 + // Check for WebSocket upgrade request
67 + if(strcasestr(v, "upgrade"))
68 + web_client_set_websocket_handshake(w);
69 }
70
71 static void http_header_dnt(struct web_client *w, const char *v, size_t len __maybe_unused) {
@@ -175,6 +179,130 @@ static void http_header_x_netdata_auth(struct web_client *w, const char *v, size
179 }
180 }
181
182 +// Handle WebSocket-specific headers
183 +static void http_header_upgrade(struct web_client *w, const char *v, size_t len __maybe_unused) {
184 + if(strcasecmp(v, "websocket") == 0) {
185 + web_client_set_websocket(w);
186 + }
187 +}
188 +
189 +static void http_header_sec_websocket_key(struct web_client *w, const char *v, size_t len __maybe_unused) {
190 + if(web_client_is_websocket(w)) {
191 + // Store the websocket key for later use in the handshake
192 + freez(w->websocket.key);
193 + w->websocket.key = strdupz(v);
194 + }
195 +}
196 +
197 +static void http_header_sec_websocket_version(struct web_client *w, const char *v, size_t len __maybe_unused) {
198 + if(web_client_is_websocket(w)) {
199 + // We only support version 13, which will be checked during handshake
200 + // No need to store this as we only accept one version
201 + if(strcmp(v, "13") != 0) {
202 + netdata_log_debug(D_WEB_CLIENT, "%llu: WebSocket version %s not supported, only version 13 is supported", w->id, v);
203 + web_client_clear_websocket(w);
204 + }
205 + }
206 +}
207 +
208 +static void http_header_sec_websocket_protocol(struct web_client *w, const char *v, size_t len __maybe_unused) {
209 + if(web_client_is_websocket(w)) {
210 + // Store the requested protocols for later evaluation during handshake
211 + w->websocket.protocol = WEBSOCKET_PROTOCOL_2id(v);
212 + }
213 +}
214 +
215 +static void http_header_sec_websocket_extensions(struct web_client *w, const char *v, size_t len __maybe_unused) {
216 + if(web_client_is_websocket(w)) {
217 + // Reset extension flags
218 + w->websocket.ext_flags = WS_EXTENSION_NONE;
219 +
220 + // Check if "permessage-deflate" is requested
221 + if (strstr(v, "permessage-deflate") != NULL) {
222 + // Parse extension parameters
223 + char extension_copy[1024];
224 + strncpy(extension_copy, v, sizeof(extension_copy) - 1);
225 + extension_copy[sizeof(extension_copy) - 1] = '\0';
226 +
227 + char *token, *saveptr;
228 + token = strtok_r(extension_copy, ",", &saveptr);
229 +
230 + while (token) {
231 + // Trim leading/trailing spaces
232 + char *ext = token;
233 + while (*ext && isspace(*ext)) ext++;
234 + char *end = ext + strlen(ext) - 1;
235 + while (end > ext && isspace(*end)) *end-- = '\0';
236 +
237 + // Check if this is permessage-deflate extension
238 + if (strncmp(ext, "permessage-deflate", 18) == 0) {
239 + w->websocket.ext_flags |= WS_EXTENSION_PERMESSAGE_DEFLATE;
240 +
241 + // Parse parameters
242 + char *params = ext + 18;
243 + if (*params == ';') {
244 + params++;
245 +
246 + char *param, *param_saveptr;
247 + param = strtok_r(params, ";", &param_saveptr);
248 +
249 + while (param) {
250 + // Trim leading/trailing spaces
251 + while (*param && isspace(*param)) param++;
252 + end = param + strlen(param) - 1;
253 + while (end > param && isspace(*end)) *end-- = '\0';
254 +
255 + // Client no context takeover
256 + if (strcmp(param, "client_no_context_takeover") == 0)
257 + w->websocket.ext_flags |= WS_EXTENSION_CLIENT_NO_CONTEXT_TAKEOVER;
258 +
259 + // Server no context takeover
260 + else if (strcmp(param, "server_no_context_takeover") == 0)
261 + w->websocket.ext_flags |= WS_EXTENSION_SERVER_NO_CONTEXT_TAKEOVER;
262 +
263 + // Server max window bits
264 + else if (strncmp(param, "server_max_window_bits=", 23) == 0) {
265 + w->websocket.server_max_window_bits = str2u(param + 23);
266 + if(w->websocket.server_max_window_bits >= 8 && w->websocket.server_max_window_bits <= 15)
267 + w->websocket.ext_flags |= WS_EXTENSION_SERVER_MAX_WINDOW_BITS;
268 + }
269 + // Server max window bits without value
270 + else if (strcmp(param, "server_max_window_bits") == 0) {
271 + w->websocket.ext_flags |= WS_EXTENSION_SERVER_MAX_WINDOW_BITS;
272 + w->websocket.server_max_window_bits = 0; // Default
273 + }
274 +
275 + // Client max window bits with value
276 + else if (strncmp(param, "client_max_window_bits=", 23) == 0) {
277 + w->websocket.client_max_window_bits = str2u(param + 23);
278 + if(w->websocket.client_max_window_bits >= 8 && w->websocket.client_max_window_bits <= 15)
279 + w->websocket.ext_flags |= WS_EXTENSION_CLIENT_MAX_WINDOW_BITS;
280 + }
281 + // Client max window bits without value
282 + else if (strcmp(param, "client_max_window_bits") == 0) {
283 + w->websocket.ext_flags |= WS_EXTENSION_CLIENT_MAX_WINDOW_BITS;
284 + w->websocket.client_max_window_bits = 0; // Default
285 + }
286 +
287 + param = strtok_r(NULL, ";", &param_saveptr);
288 + }
289 + }
290 +
291 + break; // Found and parsed permessage-deflate
292 + }
293 +
294 + token = strtok_r(NULL, ",", &saveptr);
295 + }
296 + }
297 +
298 + netdata_log_debug(D_WEB_CLIENT, "%llu: Client requested WebSocket extensions: %s, "
299 + "enabled flags: %u, client_max_window_bits: %u, server_max_window_bits: %u",
300 + w->id, v, w->websocket.ext_flags,
301 + w->websocket.client_max_window_bits,
302 + w->websocket.server_max_window_bits);
303 + }
304 +}
305 +
306 struct {
307 uint32_t hash;
308 const char *key;
@@ -196,6 +324,13 @@ struct {
324 { .hash = 0, .key = "X-Netdata-User-Name", .cb = http_header_x_netdata_user_name },
325 { .hash = 0, .key = "X-Netdata-Auth", .cb = http_header_x_netdata_auth },
326
327 + // WebSocket headers
328 + { .hash = 0, .key = "Upgrade", .cb = http_header_upgrade },
329 + { .hash = 0, .key = "Sec-WebSocket-Key", .cb = http_header_sec_websocket_key },
330 + { .hash = 0, .key = "Sec-WebSocket-Version", .cb = http_header_sec_websocket_version },
331 + { .hash = 0, .key = "Sec-WebSocket-Protocol",.cb = http_header_sec_websocket_protocol },
332 + { .hash = 0, .key = "Sec-WebSocket-Extensions",.cb = http_header_sec_websocket_extensions },
333 +
334 // for historical reasons.
335 // there are a few nightly versions of netdata UI that incorrectly use this instead of X-Netdata-Auth
336 { .hash = 0, .key = "Authorization", .cb = http_header_x_netdata_auth },
src/web/mcp/adapters/mcp-websocket.c new
+124
@@ -0,0 +1,124 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "mcp-websocket.h"
4 +#include "web/websocket/websocket-internal.h"
5 +
6 +// Store the MCP context in the WebSocket client's data field
7 +void mcp_websocket_set_context(struct websocket_server_client *wsc, MCP_CLIENT *ctx) {
8 + if (!wsc) return;
9 + wsc->user_data = ctx;
10 +}
11 +
12 +// Get the MCP context from a WebSocket client
13 +MCP_CLIENT *mcp_websocket_get_context(struct websocket_server_client *wsc) {
14 + if (!wsc) return NULL;
15 + return (MCP_CLIENT *)wsc->user_data;
16 +}
17 +
18 +// WebSocket buffer sender function for the MCP adapter
19 +int mcp_websocket_send_buffer(struct websocket_server_client *wsc, BUFFER *buffer) {
20 + if (!wsc || !buffer) return -1;
21 +
22 + const char *text = buffer_tostring(buffer);
23 + if (!text || !*text) return -1;
24 +
25 + return websocket_protocol_send_text(wsc, text);
26 +}
27 +
28 +// Create a response context for a WebSocket client
29 +static MCP_CLIENT *mcp_websocket_create_context(struct websocket_server_client *wsc) {
30 + if (!wsc) return NULL;
31 +
32 + MCP_CLIENT *ctx = mcp_create_client(MCP_TRANSPORT_WEBSOCKET, wsc);
33 + mcp_websocket_set_context(wsc, ctx);
34 +
35 + return ctx;
36 +}
37 +
38 +// WebSocket connection handler for MCP
39 +void mcp_websocket_on_connect(struct websocket_server_client *wsc) {
40 + if (!wsc) return;
41 +
42 + // Create the MCP context
43 + MCP_CLIENT *ctx = mcp_websocket_create_context(wsc);
44 + if (!ctx) {
45 + websocket_protocol_send_close(wsc, WS_CLOSE_INTERNAL_ERROR, "Failed to create MCP context");
46 + return;
47 + }
48 +
49 + websocket_debug(wsc, "MCP client connected");
50 +}
51 +
52 +// WebSocket message handler for MCP - receives message and routes to MCP
53 +void mcp_websocket_on_message(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode) {
54 + if (!wsc || !message || length == 0) return;
55 +
56 + // Only handle text messages
57 + if (opcode != WS_OPCODE_TEXT) {
58 + websocket_debug(wsc, "Ignoring binary message");
59 + return;
60 + }
61 +
62 + // Get the MCP context
63 + MCP_CLIENT *ctx = mcp_websocket_get_context(wsc);
64 + if (!ctx) {
65 + websocket_debug(wsc, "MCP context not found");
66 + websocket_protocol_send_close(wsc, WS_CLOSE_INTERNAL_ERROR, "MCP context not found");
67 + return;
68 + }
69 +
70 + // Parse the JSON-RPC request
71 + struct json_object *request = NULL;
72 + enum json_tokener_error jerr = json_tokener_success;
73 + request = json_tokener_parse_verbose(message, &jerr);
74 +
75 + if (!request || jerr != json_tokener_success) {
76 + websocket_debug(wsc, "Failed to parse JSON-RPC request: %s", json_tokener_error_desc(jerr));
77 + CLEAN_BUFFER *b = buffer_create(0, NULL);
78 + mcp_jsonrpc_error(b, NULL, 0, -32700);
79 + mcp_websocket_send_buffer(wsc, b);
80 + return;
81 + }
82 +
83 + // Pass the request to the MCP handler
84 + mcp_handle_request(ctx, request);
85 +
86 + // Free the request object
87 + json_object_put(request);
88 +}
89 +
90 +// WebSocket close handler for MCP
91 +void mcp_websocket_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason) {
92 + if (!wsc) return;
93 +
94 + websocket_debug(wsc, "MCP client closing (code: %d, reason: %s)", code, reason ? reason : "none");
95 +
96 + // Clean up the MCP context
97 + MCP_CLIENT *ctx = mcp_websocket_get_context(wsc);
98 + if (ctx) {
99 + mcp_free_client(ctx);
100 + mcp_websocket_set_context(wsc, NULL);
101 + }
102 +}
103 +
104 +// WebSocket disconnect handler for MCP
105 +void mcp_websocket_on_disconnect(struct websocket_server_client *wsc) {
106 + if (!wsc) return;
107 +
108 + websocket_debug(wsc, "MCP client disconnected");
109 +
110 + // Clean up the MCP context
111 + MCP_CLIENT *ctx = mcp_websocket_get_context(wsc);
112 + if (ctx) {
113 + mcp_free_client(ctx);
114 + mcp_websocket_set_context(wsc, NULL);
115 + }
116 +}
117 +
118 +// Register WebSocket callbacks for MCP
119 +void mcp_websocket_adapter_initialize(void) {
120 + // Initialize the MCP subsystem
121 + mcp_initialize_subsystem();
122 +
123 + netdata_log_info("MCP WebSocket adapter initialized");
124 +}
\ No newline at end of file
src/web/mcp/adapters/mcp-websocket.h new
+30
@@ -0,0 +1,30 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_ADAPTER_WEBSOCKET_H
4 +#define NETDATA_MCP_ADAPTER_WEBSOCKET_H
5 +
6 +#include "web/websocket/websocket-internal.h"
7 +#include "web/mcp/mcp.h"
8 +
9 +// Initialize the WebSocket adapter for MCP
10 +void mcp_websocket_adapter_initialize(void);
11 +
12 +// WebSocket protocol handler callbacks for MCP
13 +void mcp_websocket_on_connect(struct websocket_server_client *wsc);
14 +void mcp_websocket_on_message(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode);
15 +void mcp_websocket_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason);
16 +void mcp_websocket_on_disconnect(struct websocket_server_client *wsc);
17 +
18 +// Helper functions for the WebSocket adapter
19 +int mcp_websocket_send_json(struct websocket_server_client *wsc, struct json_object *json);
20 +int mcp_websocket_send_buffer(struct websocket_server_client *wsc, BUFFER *buffer);
21 +
22 +// Get and set MCP context from a WebSocket client
23 +MCP_CLIENT *mcp_websocket_get_context(struct websocket_server_client *wsc);
24 +void mcp_websocket_set_context(struct websocket_server_client *wsc, MCP_CLIENT *ctx);
25 +
26 +// Convenience wrappers for sending responses
27 +void mcp_websocket_send_error_response(struct websocket_server_client *wsc, int code, const char *message, uint64_t id);
28 +void mcp_websocket_send_success_response(struct websocket_server_client *wsc, struct json_object *result, uint64_t id);
29 +
30 +#endif // NETDATA_MCP_ADAPTER_WEBSOCKET_H
\ No newline at end of file
src/web/mcp/mcp-context.c new
+102
@@ -0,0 +1,102 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +/**
4 + * MCP Context Namespace
5 + *
6 + * The MCP Context namespace provides methods for managing contextual information
7 + * exchanged between clients and servers. Context represents stateful information
8 + * that enhances the interaction between components.
9 + *
10 + * Key features of the context namespace:
11 + *
12 + * 1. Context Management:
13 + * - Provide contextual information to the server (context/provide)
14 + * - Clear specific context data (context/clear)
15 + * - Check the status of current context (context/status)
16 + *
17 + * 2. Context Persistence:
18 + * - Save context for future use (context/save)
19 + * - Load previously saved context (context/load)
20 + *
21 + * Context in MCP can include:
22 + * - User preferences and settings
23 + * - Session-specific information
24 + * - Authentication and authorization details
25 + * - Client capabilities and limitations
26 + * - Conversation or interaction history
27 + *
28 + * In the Netdata environment, context might include:
29 + * - User display preferences (theme, date formats, etc.)
30 + * - View configurations (dashboard layouts, chart settings)
31 + * - Filtering and query preferences
32 + * - Historical interaction patterns
33 + * - Authentication tokens and permissions
34 + *
35 + * Context can be transient (per session) or persistent (saved across sessions),
36 + * and may be scoped to specific interactions or broadly applied.
37 + */
38 +
39 +#include "mcp-context.h"
40 +#include "mcp-initialize.h"
41 +
42 +// Stub implementations for all context namespace methods (transport-agnostic)
43 +static MCP_RETURN_CODE mcp_context_method_provide(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
44 + buffer_sprintf(mcpc->error, "Method 'context/provide' not implemented yet");
45 + return MCP_RC_NOT_IMPLEMENTED;
46 +}
47 +
48 +static MCP_RETURN_CODE mcp_context_method_clear(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
49 + buffer_sprintf(mcpc->error, "Method 'context/clear' not implemented yet");
50 + return MCP_RC_NOT_IMPLEMENTED;
51 +}
52 +
53 +static MCP_RETURN_CODE mcp_context_method_status(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
54 + buffer_sprintf(mcpc->error, "Method 'context/status' not implemented yet");
55 + return MCP_RC_NOT_IMPLEMENTED;
56 +}
57 +
58 +static MCP_RETURN_CODE mcp_context_method_save(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
59 + buffer_sprintf(mcpc->error, "Method 'context/save' not implemented yet");
60 + return MCP_RC_NOT_IMPLEMENTED;
61 +}
62 +
63 +static MCP_RETURN_CODE mcp_context_method_load(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
64 + buffer_sprintf(mcpc->error, "Method 'context/load' not implemented yet");
65 + return MCP_RC_NOT_IMPLEMENTED;
66 +}
67 +
68 +// Context namespace method dispatcher (transport-agnostic)
69 +MCP_RETURN_CODE mcp_context_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id) {
70 + if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
71 +
72 + netdata_log_debug(D_MCP, "MCP context method: %s", method);
73 +
74 + // Flush previous buffers
75 + buffer_flush(mcpc->result);
76 + buffer_flush(mcpc->error);
77 +
78 + MCP_RETURN_CODE rc;
79 +
80 + if (strcmp(method, "provide") == 0) {
81 + rc = mcp_context_method_provide(mcpc, params, id);
82 + }
83 + else if (strcmp(method, "clear") == 0) {
84 + rc = mcp_context_method_clear(mcpc, params, id);
85 + }
86 + else if (strcmp(method, "status") == 0) {
87 + rc = mcp_context_method_status(mcpc, params, id);
88 + }
89 + else if (strcmp(method, "save") == 0) {
90 + rc = mcp_context_method_save(mcpc, params, id);
91 + }
92 + else if (strcmp(method, "load") == 0) {
93 + rc = mcp_context_method_load(mcpc, params, id);
94 + }
95 + else {
96 + // Method not found in context namespace
97 + buffer_sprintf(mcpc->error, "Method 'context/%s' not implemented yet", method);
98 + rc = MCP_RC_NOT_IMPLEMENTED;
99 + }
100 +
101 + return rc;
102 +}
src/web/mcp/mcp-context.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_CONTEXT_H
4 +#define NETDATA_MCP_CONTEXT_H
5 +
6 +#include "mcp.h"
7 +
8 +// Context namespace method dispatcher (transport-agnostic)
9 +MCP_RETURN_CODE mcp_context_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_CONTEXT_H
src/web/mcp/mcp-initialize.c new
+252
@@ -0,0 +1,252 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "mcp-initialize.h"
4 +#include "database/rrd-metadata.h"
5 +#include "database/rrd-retention.h"
6 +#include "daemon/common.h"
7 +
8 +// Initialize handler - provides information about what's available (transport-agnostic)
9 +MCP_RETURN_CODE mcp_method_initialize(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
10 + if (!mcpc) {
11 + buffer_strcat(mcpc->error, "Invalid MCP client context");
12 + return MCP_RC_ERROR;
13 + }
14 +
15 + // Extract client's requested protocol version
16 + struct json_object *protocol_version_obj = NULL;
17 + if (json_object_object_get_ex(params, "protocolVersion", &protocol_version_obj)) {
18 + const char *version_str = json_object_get_string(protocol_version_obj);
19 +
20 + // Convert to our enum
21 + mcpc->protocol_version = MCP_PROTOCOL_VERSION_2id(version_str);
22 +
23 + // If unknown version, default to the latest we support
24 + if (mcpc->protocol_version == MCP_PROTOCOL_VERSION_UNKNOWN) {
25 + mcpc->protocol_version = MCP_PROTOCOL_VERSION_LATEST;
26 + }
27 + } else {
28 + // No version specified, default to oldest version for compatibility
29 + mcpc->protocol_version = MCP_PROTOCOL_VERSION_2024_11_05;
30 + }
31 +
32 + netdata_log_debug(D_MCP, "MCP initialize request from client %s version %s, protocol version %s",
33 + string2str(mcpc->client_name), string2str(mcpc->client_version),
34 + MCP_PROTOCOL_VERSION_2str(mcpc->protocol_version));
35 +
36 + // Initialize result buffer with JSON structure
37 + mcp_init_success_result(mcpc, id);
38 +
39 + // Use rrdstats_metadata_collect to get infrastructure statistics
40 + RRDSTATS_METADATA metadata = rrdstats_metadata_collect();
41 +
42 + // Use rrdstats_retention_collect to get retention information
43 + RRDSTATS_RETENTION retention = rrdstats_retention_collect();
44 +
45 + buffer_json_member_add_object(mcpc->result, "result");
46 +
47 + // Add protocol version based on what client requested
48 + buffer_json_member_add_string(mcpc->result, "protocolVersion",
49 + MCP_PROTOCOL_VERSION_2str(mcpc->protocol_version));
50 +
51 + // Add server info object
52 + buffer_json_member_add_object(mcpc->result, "serverInfo");
53 + buffer_json_member_add_string(mcpc->result, "name", "Netdata");
54 + buffer_json_member_add_string(mcpc->result, "version", NETDATA_VERSION);
55 + buffer_json_object_close(mcpc->result); // Close serverInfo
56 +
57 + // Add capabilities object according to MCP standard
58 + buffer_json_member_add_object(mcpc->result, "capabilities");
59 +
60 + // Tools capabilities
61 + buffer_json_member_add_object(mcpc->result, "tools");
62 + buffer_json_member_add_boolean(mcpc->result, "listChanged", false);
63 + buffer_json_member_add_boolean(mcpc->result, "asyncExecution", true);
64 + buffer_json_member_add_boolean(mcpc->result, "batchExecution", true);
65 + buffer_json_object_close(mcpc->result); // Close tools
66 +
67 + // Resources capabilities
68 + buffer_json_member_add_object(mcpc->result, "resources");
69 + buffer_json_member_add_boolean(mcpc->result, "listChanged", true);
70 + buffer_json_member_add_boolean(mcpc->result, "subscribe", true);
71 + buffer_json_object_close(mcpc->result); // Close resources
72 +
73 + // Prompts capabilities
74 + buffer_json_member_add_object(mcpc->result, "prompts");
75 + buffer_json_member_add_boolean(mcpc->result, "listChanged", false);
76 + buffer_json_object_close(mcpc->result); // Close prompts
77 +
78 + // Notification capabilities
79 + buffer_json_member_add_object(mcpc->result, "notifications");
80 + buffer_json_member_add_boolean(mcpc->result, "push", true);
81 + buffer_json_member_add_boolean(mcpc->result, "subscription", true);
82 + buffer_json_object_close(mcpc->result); // Close notifications
83 +
84 + // Add logging capabilities
85 + buffer_json_member_add_object(mcpc->result, "logging");
86 + buffer_json_object_close(mcpc->result); // Close logging
87 +
88 + // Add version-specific capabilities
89 + if (mcpc->protocol_version >= MCP_PROTOCOL_VERSION_2025_03_26) {
90 + // Add completions capability - new in 2025-03-26
91 + buffer_json_member_add_object(mcpc->result, "completions");
92 + buffer_json_object_close(mcpc->result); // Close completions
93 + }
94 +
95 + buffer_json_object_close(mcpc->result); // Close capabilities
96 +
97 + // Add dynamic instructions based on server profile
98 + char instructions[1024];
99 +
100 + const char *common =
101 + "Use the resources to identify the systems, components and applications being monitored,\n"
102 + "and the alerts that have been configured.\n"
103 + "\n"
104 + "Use the tools to perform queries on metrics and logs, seek for outliers and anomalies,\n"
105 + "perform root cause analysis and get live information about processes, network connections,\n"
106 + "containers, VMs, systemd/windows services, sensors, kubernetes clusters, and more.\n"
107 + "\n"
108 + "Tools can also help in investigating currently raised alerts and their past transitions.";
109 +
110 + // Determine server role based on metadata
111 + if (metadata.nodes.total > 1) {
112 + // This is a parent node with child nodes streaming to it
113 + snprintfz(instructions, sizeof(instructions),
114 + "This is a Netdata Parent Server hosting metrics and logs for %zu node%s.\n\n%s",
115 + metadata.nodes.total, (metadata.nodes.total == 1) ? "" : "s", common);
116 + }
117 + else {
118 + // This is a standalone server
119 + snprintfz(instructions, sizeof(instructions),
120 + "This is Netdata on a Standalone Server.\n\n%s", common);
121 + }
122 +
123 + buffer_json_member_add_string(mcpc->result, "instructions", instructions);
124 +
125 + // Add _meta field (optional)
126 + buffer_json_member_add_object(mcpc->result, "_meta");
127 + buffer_json_member_add_string(mcpc->result, "generator", "netdata");
128 +
129 + // Get current time and calculate uptimes
130 + time_t now = now_realtime_sec();
131 + time_t system_uptime_seconds = now_boottime_sec();
132 + time_t netdata_uptime_seconds = now - netdata_start_time;
133 +
134 + buffer_json_member_add_int64(mcpc->result, "timestamp", (int64_t)now);
135 +
136 + // Add system uptime info - both raw seconds and human-readable format
137 + char human_readable[128];
138 + duration_snprintf_time_t(human_readable, sizeof(human_readable), system_uptime_seconds);
139 +
140 + buffer_json_member_add_object(mcpc->result, "system_uptime");
141 + buffer_json_member_add_int64(mcpc->result, "seconds", (int64_t)system_uptime_seconds);
142 + buffer_json_member_add_string(mcpc->result, "human", human_readable);
143 + buffer_json_object_close(mcpc->result); // Close system_uptime
144 +
145 + // Add netdata uptime info - both raw seconds and human-readable format
146 + duration_snprintf_time_t(human_readable, sizeof(human_readable), netdata_uptime_seconds);
147 +
148 + buffer_json_member_add_object(mcpc->result, "netdata_uptime");
149 + buffer_json_member_add_int64(mcpc->result, "seconds", (int64_t)netdata_uptime_seconds);
150 + buffer_json_member_add_string(mcpc->result, "human", human_readable);
151 + buffer_json_object_close(mcpc->result); // Close netdata_uptime
152 +
153 + // Add infrastructure statistics to metadata
154 + buffer_json_member_add_object(mcpc->result, "infrastructure");
155 +
156 + // Add nodes statistics
157 + buffer_json_member_add_object(mcpc->result, "nodes");
158 + buffer_json_member_add_int64(mcpc->result, "total", metadata.nodes.total);
159 + buffer_json_member_add_int64(mcpc->result, "receiving_from_children", metadata.nodes.receiving);
160 + buffer_json_member_add_int64(mcpc->result, "sending_to_next_parent", metadata.nodes.sending);
161 + buffer_json_member_add_int64(mcpc->result, "archived_but_available_for_queries", metadata.nodes.archived);
162 + buffer_json_member_add_string(mcpc->result, "info", "Nodes (or hosts, or servers, or devices) are Netdata Agent installations or virtual Netdata nodes or SNMP devices.");
163 + buffer_json_object_close(mcpc->result); // Close nodes
164 +
165 + // Add metrics statistics
166 + buffer_json_member_add_object(mcpc->result, "metrics");
167 + buffer_json_member_add_int64(mcpc->result, "currently_being_collected", metadata.metrics.collected);
168 + buffer_json_member_add_int64(mcpc->result, "total_available_for_queries", metadata.metrics.available);
169 + buffer_json_member_add_string(mcpc->result, "info", "Metrics are unique time-series in the Netdata time-series database.");
170 + buffer_json_object_close(mcpc->result); // Close metrics
171 +
172 + // Add instances statistics
173 + buffer_json_member_add_object(mcpc->result, "instances");
174 + buffer_json_member_add_int64(mcpc->result, "currently_being_collected", metadata.instances.collected);
175 + buffer_json_member_add_int64(mcpc->result, "total_available_for_queries", metadata.instances.available);
176 + buffer_json_member_add_string(mcpc->result, "info", "Instances are collections of metrics referring to a component (system, disk, network interface, application, process, container, etc).");
177 + buffer_json_object_close(mcpc->result); // Close instances
178 +
179 + // Add contexts statistics
180 + buffer_json_member_add_object(mcpc->result, "contexts");
181 + buffer_json_member_add_int64(mcpc->result, "unique_across_all_nodes", metadata.contexts.unique);
182 + buffer_json_member_add_string(mcpc->result, "info", "Contexts are distinct charts shown on the Netdata dashboards, like system.cpu (system CPU utilization), or net.net (network interfaces bandwidth). When monitoring applications, the context usually includes the application name.");
183 + buffer_json_object_close(mcpc->result); // Close contexts
184 +
185 + // Add retention information
186 + if (retention.storage_tiers > 0) {
187 + buffer_json_member_add_object(mcpc->result, "retention");
188 + buffer_json_member_add_array(mcpc->result, "tiers");
189 +
190 + for (size_t i = 0; i < retention.storage_tiers; i++) {
191 + RRD_STORAGE_TIER *tier_info = &retention.tiers[i];
192 +
193 + // Skip empty tiers
194 + if (tier_info->metrics == 0 && tier_info->samples == 0)
195 + continue;
196 +
197 + buffer_json_add_array_item_object(mcpc->result);
198 +
199 + // Add basic tier info
200 + buffer_json_member_add_int64(mcpc->result, "tier", tier_info->tier);
201 + buffer_json_member_add_string(mcpc->result, "backend",
202 + tier_info->backend == STORAGE_ENGINE_BACKEND_DBENGINE ? "dbengine" :
203 + tier_info->backend == STORAGE_ENGINE_BACKEND_RRDDIM ? "ram" : "unknown");
204 + buffer_json_member_add_int64(mcpc->result, "granularity", tier_info->group_seconds);
205 + buffer_json_member_add_string(mcpc->result, "granularity_human", tier_info->granularity_human);
206 +
207 + // Add metrics info
208 + buffer_json_member_add_int64(mcpc->result, "metrics", tier_info->metrics);
209 + buffer_json_member_add_int64(mcpc->result, "samples", tier_info->samples);
210 +
211 + // Add storage info when available
212 + if (tier_info->disk_max > 0) {
213 + buffer_json_member_add_int64(mcpc->result, "disk_used", tier_info->disk_used);
214 + buffer_json_member_add_int64(mcpc->result, "disk_max", tier_info->disk_max);
215 + // Format disk_percent to have only 2 decimal places
216 + double rounded_percent = floor(tier_info->disk_percent * 100.0 + 0.5) / 100.0;
217 + buffer_json_member_add_double(mcpc->result, "disk_percent", rounded_percent);
218 + }
219 +
220 + // Add retention info
221 + if (tier_info->retention > 0) {
222 + buffer_json_member_add_int64(mcpc->result, "first_time_s", tier_info->first_time_s);
223 + buffer_json_member_add_int64(mcpc->result, "last_time_s", tier_info->last_time_s);
224 + buffer_json_member_add_int64(mcpc->result, "retention", tier_info->retention);
225 + buffer_json_member_add_string(mcpc->result, "retention_human", tier_info->retention_human);
226 +
227 + if (tier_info->requested_retention > 0) {
228 + buffer_json_member_add_int64(mcpc->result, "requested_retention", tier_info->requested_retention);
229 + buffer_json_member_add_string(mcpc->result, "requested_retention_human", tier_info->requested_retention_human);
230 + }
231 +
232 + if (tier_info->expected_retention > 0) {
233 + buffer_json_member_add_int64(mcpc->result, "expected_retention", tier_info->expected_retention);
234 + buffer_json_member_add_string(mcpc->result, "expected_retention_human", tier_info->expected_retention_human);
235 + }
236 + }
237 +
238 + buffer_json_object_close(mcpc->result); // Close tier object
239 + }
240 +
241 + buffer_json_array_close(mcpc->result); // Close tiers array
242 + buffer_json_member_add_string(mcpc->result, "info", "Metrics retention information for each storage tier in the Netdata database.\nHigher tiers can provide min, max, average, sum and anomaly rate with the same accuracy as tier 0.\nTiers are automatically selected during query.");
243 + buffer_json_object_close(mcpc->result); // Close retention
244 + }
245 +
246 + buffer_json_object_close(mcpc->result); // Close infrastructure
247 + buffer_json_object_close(mcpc->result); // Close _meta
248 + buffer_json_object_close(mcpc->result); // Close result object
249 + buffer_json_finalize(mcpc->result); // Finalize JSON
250 +
251 + return MCP_RC_OK;
252 +}
src/web/mcp/mcp-initialize.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_INITIALIZE_H
4 +#define NETDATA_MCP_INITIALIZE_H
5 +
6 +#include "mcp.h"
7 +
8 +// Initialize method handler (transport-agnostic)
9 +MCP_RETURN_CODE mcp_method_initialize(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_INITIALIZE_H
\ No newline at end of file
src/web/mcp/mcp-notifications.c new
+131
@@ -0,0 +1,131 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +/**
4 + * MCP Notifications Namespace
5 + *
6 + * The MCP Notifications namespace provides methods for managing and handling notifications.
7 + * In the MCP protocol, notifications enable real-time communication of events from server to client,
8 + * and from client to server.
9 + *
10 + * Key features of the notifications namespace:
11 + *
12 + * 1. Initialization:
13 + * - Clients notify the server when they're initialized (notifications/initialized)
14 + * - This initiates the notification subsystem
15 + *
16 + * 2. Subscription Management:
17 + * - Subscribe to specific notification types (notifications/subscribe)
18 + * - Unsubscribe from notifications (notifications/unsubscribe)
19 + * - Configure notification settings (notifications/getSettings)
20 + *
21 + * 3. Notification Handling:
22 + * - Acknowledge received notifications (notifications/acknowledge)
23 + * - View notification history (notifications/getHistory)
24 + * - Send notifications from client to server (notifications/send)
25 + *
26 + * Notifications in MCP are bidirectional:
27 + * - Server-to-client notifications inform about system events, alerts, changes
28 + * - Client-to-server notifications provide user actions and status updates
29 + *
30 + * In the Netdata context, notifications include:
31 + * - Health monitoring alerts
32 + * - System status changes
33 + * - Configuration changes
34 + * - Resource availability updates
35 + * - Client status updates
36 + *
37 + * Notifications can be transient or persistent, prioritized, and may require
38 + * acknowledgment depending on their type and importance.
39 + */
40 +
41 +#include "mcp-notifications.h"
42 +#include "mcp-initialize.h"
43 +
44 +// Implementation of notifications/initialized (transport-agnostic)
45 +static MCP_RETURN_CODE mcp_notifications_method_initialized(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
46 + // This is just a notification, just log it
47 + netdata_log_debug(D_MCP, "Client sent notifications/initialized notification");
48 +
49 + // No response needed if this is a notification (id == 0)
50 + if (id == 0) return MCP_RC_OK;
51 +
52 + // If it was a request (has id), send an empty success response
53 + mcp_init_success_result(mcpc, id);
54 + buffer_json_finalize(mcpc->result);
55 +
56 + return MCP_RC_OK;
57 +}
58 +
59 +// Stub implementations for other notifications methods (transport-agnostic)
60 +static MCP_RETURN_CODE mcp_notifications_method_subscribe(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
61 + buffer_sprintf(mcpc->error, "Method 'notifications/subscribe' not implemented yet");
62 + return MCP_RC_NOT_IMPLEMENTED;
63 +}
64 +
65 +static MCP_RETURN_CODE mcp_notifications_method_unsubscribe(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
66 + buffer_sprintf(mcpc->error, "Method 'notifications/unsubscribe' not implemented yet");
67 + return MCP_RC_NOT_IMPLEMENTED;
68 +}
69 +
70 +static MCP_RETURN_CODE mcp_notifications_method_acknowledge(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
71 + buffer_sprintf(mcpc->error, "Method 'notifications/acknowledge' not implemented yet");
72 + return MCP_RC_NOT_IMPLEMENTED;
73 +}
74 +
75 +static MCP_RETURN_CODE mcp_notifications_method_getHistory(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
76 + buffer_sprintf(mcpc->error, "Method 'notifications/getHistory' not implemented yet");
77 + return MCP_RC_NOT_IMPLEMENTED;
78 +}
79 +
80 +static MCP_RETURN_CODE mcp_notifications_method_send(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
81 + buffer_sprintf(mcpc->error, "Method 'notifications/send' not implemented yet");
82 + return MCP_RC_NOT_IMPLEMENTED;
83 +}
84 +
85 +static MCP_RETURN_CODE mcp_notifications_method_getSettings(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
86 + buffer_sprintf(mcpc->error, "Method 'notifications/getSettings' not implemented yet");
87 + return MCP_RC_NOT_IMPLEMENTED;
88 +}
89 +
90 +// Notifications namespace method dispatcher (transport-agnostic)
91 +MCP_RETURN_CODE mcp_notifications_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id) {
92 + if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
93 +
94 + netdata_log_debug(D_MCP, "MCP notifications method: %s", method);
95 +
96 + // Flush previous buffers
97 + buffer_flush(mcpc->result);
98 + buffer_flush(mcpc->error);
99 +
100 + MCP_RETURN_CODE rc;
101 +
102 + if (strcmp(method, "initialized") == 0) {
103 + rc = mcp_notifications_method_initialized(mcpc, params, id);
104 + }
105 + else if (strcmp(method, "subscribe") == 0) {
106 + rc = mcp_notifications_method_subscribe(mcpc, params, id);
107 + }
108 + else if (strcmp(method, "unsubscribe") == 0) {
109 + rc = mcp_notifications_method_unsubscribe(mcpc, params, id);
110 + }
111 + else if (strcmp(method, "acknowledge") == 0) {
112 + rc = mcp_notifications_method_acknowledge(mcpc, params, id);
113 + }
114 + else if (strcmp(method, "getHistory") == 0) {
115 + rc = mcp_notifications_method_getHistory(mcpc, params, id);
116 + }
117 + else if (strcmp(method, "send") == 0) {
118 + rc = mcp_notifications_method_send(mcpc, params, id);
119 + }
120 + else if (strcmp(method, "getSettings") == 0) {
121 + rc = mcp_notifications_method_getSettings(mcpc, params, id);
122 + }
123 + else {
124 + // Method not found in notifications namespace
125 + buffer_sprintf(mcpc->error, "Method 'notifications/%s' not implemented yet", method);
126 + rc = MCP_RC_NOT_IMPLEMENTED;
127 + }
128 +
129 + return rc;
130 +}
131 +
src/web/mcp/mcp-notifications.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_NOTIFICATIONS_H
4 +#define NETDATA_MCP_NOTIFICATIONS_H
5 +
6 +#include "mcp.h"
7 +
8 +// Notifications namespace method dispatcher (transport-agnostic)
9 +MCP_RETURN_CODE mcp_notifications_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_NOTIFICATIONS_H
\ No newline at end of file
src/web/mcp/mcp-prompts.c new
+131
@@ -0,0 +1,131 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +/**
4 + * MCP Prompts Namespace
5 + *
6 + * The MCP Prompts namespace provides methods for managing and executing prompts.
7 + * In the MCP protocol, prompts are text templates that guide AI generation for specific tasks.
8 + * Prompts are user-controlled interactions that leverage AI capabilities in predefined ways.
9 + *
10 + * Key features of the prompts namespace:
11 + *
12 + * 1. Prompt Management:
13 + * - List available prompts (prompts/list)
14 + * - Get details about specific prompts (prompts/get)
15 + * - Save custom prompts (prompts/save)
16 + * - Delete prompts (prompts/delete)
17 + * - Organize prompts into categories (prompts/getCategories)
18 + *
19 + * 2. Prompt Execution:
20 + * - Execute prompts with input parameters (prompts/execute)
21 + * - View execution history (prompts/getHistory)
22 + *
23 + * Prompts differ from tools in that they are:
24 + * - More flexible and text-oriented
25 + * - Designed for natural language processing
26 + * - Often used for analysis and summarization
27 + * - Usually invoked explicitly by users rather than by the model
28 + *
29 + * In the Netdata context, prompts might include:
30 + * - Analyzing a time period of metrics for anomalies
31 + * - Summarizing system health
32 + * - Creating natural language explanations of charts
33 + * - Helping users create custom alert configurations
34 + * - Generating analysis reports
35 + *
36 + * Prompts typically use templating to insert user-provided context into predefined templates,
37 + * making them powerful for specific analysis tasks while maintaining predictable outputs.
38 + */
39 +
40 +#include "mcp-prompts.h"
41 +#include "mcp-initialize.h"
42 +
43 +// Implementation of prompts/list (transport-agnostic)
44 +static MCP_RETURN_CODE mcp_prompts_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
45 + if (!mcpc || id == 0) return MCP_RC_ERROR;
46 +
47 + // Initialize success response
48 + mcp_init_success_result(mcpc, id);
49 +
50 + // Add empty prompts array
51 + buffer_json_member_add_array(mcpc->result, "prompts");
52 + buffer_json_array_close(mcpc->result); // Close prompts array
53 +
54 + // Close the result object
55 + buffer_json_finalize(mcpc->result);
56 +
57 + return MCP_RC_OK;
58 +}
59 +
60 +// Stub implementations for other prompts methods (transport-agnostic)
61 +static MCP_RETURN_CODE mcp_prompts_method_execute(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
62 + buffer_sprintf(mcpc->error, "Method 'prompts/execute' not implemented yet");
63 + return MCP_RC_NOT_IMPLEMENTED;
64 +}
65 +
66 +static MCP_RETURN_CODE mcp_prompts_method_get(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
67 + buffer_sprintf(mcpc->error, "Method 'prompts/get' not implemented yet");
68 + return MCP_RC_NOT_IMPLEMENTED;
69 +}
70 +
71 +static MCP_RETURN_CODE mcp_prompts_method_save(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
72 + buffer_sprintf(mcpc->error, "Method 'prompts/save' not implemented yet");
73 + return MCP_RC_NOT_IMPLEMENTED;
74 +}
75 +
76 +static MCP_RETURN_CODE mcp_prompts_method_delete(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
77 + buffer_sprintf(mcpc->error, "Method 'prompts/delete' not implemented yet");
78 + return MCP_RC_NOT_IMPLEMENTED;
79 +}
80 +
81 +static MCP_RETURN_CODE mcp_prompts_method_getCategories(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
82 + buffer_sprintf(mcpc->error, "Method 'prompts/getCategories' not implemented yet");
83 + return MCP_RC_NOT_IMPLEMENTED;
84 +}
85 +
86 +static MCP_RETURN_CODE mcp_prompts_method_getHistory(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
87 + buffer_sprintf(mcpc->error, "Method 'prompts/getHistory' not implemented yet");
88 + return MCP_RC_NOT_IMPLEMENTED;
89 +}
90 +
91 +// Prompts namespace method dispatcher (transport-agnostic)
92 +MCP_RETURN_CODE mcp_prompts_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id) {
93 + if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
94 +
95 + netdata_log_debug(D_MCP, "MCP prompts method: %s", method);
96 +
97 + // Flush previous buffers
98 + buffer_flush(mcpc->result);
99 + buffer_flush(mcpc->error);
100 +
101 + MCP_RETURN_CODE rc;
102 +
103 + if (strcmp(method, "list") == 0) {
104 + rc = mcp_prompts_method_list(mcpc, params, id);
105 + }
106 + else if (strcmp(method, "execute") == 0) {
107 + rc = mcp_prompts_method_execute(mcpc, params, id);
108 + }
109 + else if (strcmp(method, "get") == 0) {
110 + rc = mcp_prompts_method_get(mcpc, params, id);
111 + }
112 + else if (strcmp(method, "save") == 0) {
113 + rc = mcp_prompts_method_save(mcpc, params, id);
114 + }
115 + else if (strcmp(method, "delete") == 0) {
116 + rc = mcp_prompts_method_delete(mcpc, params, id);
117 + }
118 + else if (strcmp(method, "getCategories") == 0) {
119 + rc = mcp_prompts_method_getCategories(mcpc, params, id);
120 + }
121 + else if (strcmp(method, "getHistory") == 0) {
122 + rc = mcp_prompts_method_getHistory(mcpc, params, id);
123 + }
124 + else {
125 + // Method not found in prompts namespace
126 + buffer_sprintf(mcpc->error, "Method 'prompts/%s' not implemented yet", method);
127 + rc = MCP_RC_NOT_IMPLEMENTED;
128 + }
129 +
130 + return rc;
131 +}
src/web/mcp/mcp-prompts.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_PROMPTS_H
4 +#define NETDATA_MCP_PROMPTS_H
5 +
6 +#include "mcp.h"
7 +
8 +// Prompts namespace method dispatcher (transport-agnostic)
9 +MCP_RETURN_CODE mcp_prompts_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_PROMPTS_H
\ No newline at end of file
src/web/mcp/mcp-resources.c new
+496
@@ -0,0 +1,496 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +/**
4 + * MCP Resources Namespace
5 + *
6 + * The MCP Resources namespace provides methods for accessing and managing resources on the server.
7 + * In the MCP protocol, resources are application-controlled data stores that provide context to the model.
8 + * Resources are passive, meaning they provide data but don't perform actions on their own.
9 + *
10 + * Key features of the resources namespace:
11 + *
12 + * 1. Resource Discovery:
13 + * - Clients can list available resources (resources/list)
14 + * - Get detailed descriptions and schemas (resources/describe, resources/getSchema)
15 + * - Search for resources matching specific criteria (resources/search)
16 + *
17 + * 2. Resource Access:
18 + * - Retrieve specific resources or portions of resources (resources/get)
19 + * - Access resources by ID or path
20 + * - Resources can be structured or unstructured
21 + *
22 + * 3. Resource Subscriptions:
23 + * - Subscribe to updates for specific resources (resources/subscribe)
24 + * - Unsubscribe from resources (resources/unsubscribe)
25 + * - Get real-time updates when subscribed resources change
26 + *
27 + * In the Netdata context, resources include:
28 + * - metrics: Time-series data collected from various sources
29 + * - logs: Log entries from system and application logs
30 + * - alerts: Health monitoring alerts and notifications
31 + * - functions: Live infrastructure snapshots providing real-time views
32 + * - nodes: Monitored infrastructure nodes with their metadata
33 + *
34 + * Resources can be hierarchical or flat, and may support different access patterns
35 + * (e.g., time-based querying for metrics, full-text search for logs).
36 + */
37 +
38 +#include "mcp-resources.h"
39 +#include "mcp-initialize.h"
40 +#include "database/contexts/rrdcontext.h"
41 +
42 +// Audience enum - bitmask for the intended audience of a resource
43 +typedef enum {
44 + RESOURCE_AUDIENCE_USER = 1 << 0, // Resource useful for users
45 + RESOURCE_AUDIENCE_ASSISTANT = 1 << 1, // Resource useful for assistants
46 + RESOURCE_AUDIENCE_BOTH = RESOURCE_AUDIENCE_USER | RESOURCE_AUDIENCE_ASSISTANT
47 +} RESOURCE_AUDIENCE;
48 +
49 +// Function pointer type for resource read callbacks
50 +typedef MCP_RETURN_CODE (*resource_read_fn)(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id);
51 +
52 +// Function pointer type for resource size callbacks
53 +typedef size_t (*resource_size_fn)(void);
54 +
55 +// Resource structure definition
56 +typedef struct {
57 + const char *name; // Resource name
58 + const char *uri; // Resource URI
59 + const char *description; // Human-readable description
60 + HTTP_CONTENT_TYPE content_type; // Content type enum
61 + RESOURCE_AUDIENCE audience; // Intended audience
62 + double priority; // Priority (0.0-1.0)
63 + resource_read_fn read_fn; // Callback function to read the resource
64 + resource_size_fn size_fn; // Optional callback function to return approximate size in bytes
65 +} MCP_RESOURCE;
66 +
67 +// Resource template structure definition
68 +typedef struct {
69 + const char *name; // Template name
70 + const char *uri_template; // URI template following RFC 6570
71 + const char *description; // Human-readable description
72 + HTTP_CONTENT_TYPE content_type; // Content type enum
73 + RESOURCE_AUDIENCE audience; // Intended audience
74 + double priority; // Priority (0.0-1.0)
75 +} MCP_RESOURCE_TEMPLATE;
76 +
77 +// Basic implementation of the contexts resource read function
78 +static MCP_RETURN_CODE mcp_resource_read_contexts(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
79 + if (!mcpc || !params || id == 0) return MCP_RC_INTERNAL_ERROR;
80 +
81 + // Extract URI from params to check for query parameters
82 + struct json_object *uri_obj = NULL;
83 + json_object_object_get_ex(params, "uri", &uri_obj);
84 + const char *uri = json_object_get_string(uri_obj);
85 +
86 + SIMPLE_PATTERN *pattern = NULL;
87 +
88 + // Check if we have a query parameter
89 + if (uri && strstr(uri, "?like=")) {
90 + const char *like_param = strstr(uri, "?like=") + 6; // Skip past "?like="
91 +
92 + // Decode the query parameter
93 + const char *decoded_query = mcp_uri_decode(mcpc, like_param);
94 +
95 + // Create a simple pattern
96 + if (decoded_query && *decoded_query)
97 + pattern = simple_pattern_create(decoded_query, "|", SIMPLE_PATTERN_EXACT, false);
98 + }
99 +
100 + mcp_init_success_result(mcpc, id);
101 + buffer_json_member_add_object(mcpc->result, "result");
102 +
103 + // Add the filtered contexts
104 + rrdcontext_context_registry_json_mcp_array(mcpc->result, pattern);
105 +
106 + // Add instructions
107 + buffer_json_member_add_string(mcpc->result, "instructions",
108 + "Additional information per context (like title, dimensions, unit, label\n"
109 + "keys and possible values, the list of nodes collecting it, and its retention)\n"
110 + "can be obtained by reading URIs in the format 'nd://contexts/{context}'\n"
111 + "(like nd://context/system.cpu.user).\n\n"
112 + "You can search contexts using glob-like patterns using the 'like' parameter:\n"
113 + "nd://contexts?like=*sql*|*db*|*redis*|*mongo*\n"
114 + "to find postgresql, mysql, mariadb and mongodb related contexts.\n\n"
115 + "For a high-level overview of monitoring categories, use nd://context-categories");
116 +
117 + buffer_json_finalize(mcpc->result);
118 +
119 + if (pattern)
120 + simple_pattern_free(pattern);
121 +
122 + return MCP_RC_OK;
123 +}
124 +
125 +// Implementation of the context categories resource read function
126 +static MCP_RETURN_CODE mcp_resource_read_context_categories(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
127 + if (!mcpc || !params || id == 0) return MCP_RC_INTERNAL_ERROR;
128 +
129 + // Extract URI from params to check for query parameters
130 + struct json_object *uri_obj = NULL;
131 + json_object_object_get_ex(params, "uri", &uri_obj);
132 + const char *uri = json_object_get_string(uri_obj);
133 +
134 + SIMPLE_PATTERN *pattern = NULL;
135 +
136 + // Check if we have a query parameter
137 + if (uri && strstr(uri, "?like=")) {
138 + const char *like_param = strstr(uri, "?like=") + 6; // Skip past "?like="
139 +
140 + // Decode the query parameter
141 + const char *decoded_query = mcp_uri_decode(mcpc, like_param);
142 +
143 + // Create a simple pattern
144 + if (decoded_query && *decoded_query)
145 + pattern = simple_pattern_create(decoded_query, "|", SIMPLE_PATTERN_EXACT, false);
146 + }
147 +
148 + mcp_init_success_result(mcpc, id);
149 + buffer_json_member_add_object(mcpc->result, "result");
150 +
151 + // Add the filtered context categories
152 + rrdcontext_context_registry_json_mcp_categories_array(mcpc->result, pattern);
153 +
154 + // Add instructions
155 + buffer_json_member_add_string(mcpc->result, "instructions",
156 + "Context categories provide a high-level overview of what's being monitored.\n"
157 + "Each category represents a group of related contexts (e.g., 'system.cpu' for CPU metrics).\n\n"
158 + "To explore all contexts within a specific category, use the pattern:\n"
159 + "nd://contexts?like={category}.*\n\n"
160 + "For example, if the cateogy is 'redis' to see all Redis-related contexts:\n"
161 + "nd://contexts?like=redis.*\n\n"
162 + "You can search categories using glob-like patterns with the 'like' parameter:\n"
163 + "nd://context-categories?like=*sql*|*db*|*mongo*\n"
164 + "to find postgresql, mysql, mariadb and mongodb related categories.");
165 +
166 + buffer_json_finalize(mcpc->result);
167 +
168 + if (pattern)
169 + simple_pattern_free(pattern);
170 +
171 + return MCP_RC_OK;
172 +}
173 +
174 +// Size estimation functions for resources
175 +static size_t mcp_resource_contexts_size(void) {
176 + CLEAN_BUFFER *wb = buffer_create(0, NULL);
177 + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
178 + rrdcontext_context_registry_json_mcp_array(wb, NULL);
179 + buffer_json_finalize(wb);
180 + return buffer_strlen(wb);
181 +}
182 +
183 +static size_t mcp_resource_context_categories_size(void) {
184 + CLEAN_BUFFER *wb = buffer_create(0, NULL);
185 + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
186 + rrdcontext_context_registry_json_mcp_categories_array(wb, NULL);
187 + buffer_json_finalize(wb);
188 + return buffer_strlen(wb);
189 +}
190 +
191 +// Static array of all available resources
192 +static const MCP_RESOURCE mcp_resources[] = {
193 + {
194 + .name = "contexts",
195 + .uri = "nd://contexts",
196 + .description =
197 + "Primary discovery mechanism for what's being monitored.\n"
198 + "Contexts are the equivalent of charts in Netdata dashboards and they are multi-node and multi-instance.\n"
199 + "Usually contexts have the same set of label keys and common or similar dimensions.\n"
200 + "Supports searches for contexts using glob-like patterns with the 'like=' parameter.\n",
201 + .content_type = CT_APPLICATION_JSON,
202 + .audience = RESOURCE_AUDIENCE_BOTH,
203 + .priority = 1.0,
204 + .read_fn = mcp_resource_read_contexts,
205 + .size_fn = mcp_resource_contexts_size
206 + },
207 + {
208 + .name = "context-categories",
209 + .uri = "nd://context-categories",
210 + .description =
211 + "High-level categories of contexts being monitored.\n"
212 + "Provides a summarized view of monitoring domains by grouping contexts by their prefix.\n"
213 + "Useful for getting a quick overview of what's being monitored without detailed breakdown.\n",
214 + .content_type = CT_APPLICATION_JSON,
215 + .audience = RESOURCE_AUDIENCE_BOTH,
216 + .priority = 0.9,
217 + .read_fn = mcp_resource_read_context_categories,
218 + .size_fn = mcp_resource_context_categories_size
219 + },
220 + // Add more resources here as they are implemented
221 + // Example:
222 + // {
223 + // .name = "nodes",
224 + // .uri = "nd://nodes",
225 + // .description = "Infrastructure discovery...",
226 + // ...
227 + // },
228 +};
229 +
230 +// Static array of all available resource templates
231 +static const MCP_RESOURCE_TEMPLATE mcp_resource_templates[] = {
232 + {
233 + .name = "Contexts Search",
234 + .uri_template = "nd://contexts{?like}",
235 + .description =
236 + "Search for monitoring contexts by matching their names against glob-like patterns.\n"
237 + "The 'like' parameter accepts pipe-separated patterns with wildcards\n"
238 + "(e.g., '?like=*sql*|*db*|*redis*|*mongo*|*{db-name}*' for common database-related contexts).",
239 + .content_type = CT_APPLICATION_JSON,
240 + .audience = RESOURCE_AUDIENCE_BOTH,
241 + .priority = 1.0
242 + },
243 + {
244 + .name = "Context Categories Search",
245 + .uri_template = "nd://context-categories{?like}",
246 + .description =
247 + "Search for high-level context categories by matching their names against glob-like patterns.\n"
248 + "The 'like' parameter accepts pipe-separated patterns with wildcards\n"
249 + "(e.g., '?like=*sql*|*db*|*redis*|*mongo*|*{db-name}*' for common database-related categories).",
250 + .content_type = CT_APPLICATION_JSON,
251 + .audience = RESOURCE_AUDIENCE_BOTH,
252 + .priority = 0.9
253 + },
254 + // Add more templates here as they are implemented
255 +};
256 +
257 +// Implementation of resources/list (transport-agnostic)
258 +static MCP_RETURN_CODE mcp_resources_method_list(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
259 + if (!mcpc || !params || !id) return MCP_RC_INTERNAL_ERROR;
260 +
261 + // Initialize success response
262 + mcp_init_success_result(mcpc, id);
263 +
264 + // Create a resources array object
265 + buffer_json_member_add_array(mcpc->result, "resources");
266 +
267 + // Iterate through our resources array and add each one
268 + for (size_t i = 0; i < _countof(mcp_resources); i++) {
269 + const MCP_RESOURCE *resource = &mcp_resources[i];
270 +
271 + buffer_json_add_array_item_object(mcpc->result);
272 +
273 + // Add required fields
274 + buffer_json_member_add_string(mcpc->result, "name", resource->name);
275 + buffer_json_member_add_string(mcpc->result, "uri", resource->uri);
276 +
277 + // Add optional fields
278 + if (resource->description) {
279 + buffer_json_member_add_string(mcpc->result, "description", resource->description);
280 + }
281 +
282 + // Convert the content_type enum to string
283 + const char *mime_type = content_type_id2string(resource->content_type);
284 + if (mime_type) {
285 + buffer_json_member_add_string(mcpc->result, "mimeType", mime_type);
286 + }
287 +
288 + // Add size information if available
289 + if (resource->size_fn) {
290 + size_t size = resource->size_fn();
291 + if (size > 0) {
292 + buffer_json_member_add_uint64(mcpc->result, "size", size);
293 + }
294 + }
295 +
296 + // Add audience annotations if specified
297 + if (resource->audience != 0) {
298 + buffer_json_member_add_object(mcpc->result, "annotations");
299 +
300 + buffer_json_member_add_array(mcpc->result, "audience");
301 +
302 + if (resource->audience & RESOURCE_AUDIENCE_USER) {
303 + buffer_json_add_array_item_string(mcpc->result, "user");
304 + }
305 +
306 + if (resource->audience & RESOURCE_AUDIENCE_ASSISTANT) {
307 + buffer_json_add_array_item_string(mcpc->result, "assistant");
308 + }
309 +
310 + buffer_json_array_close(mcpc->result); // Close audience array
311 +
312 + // Add priority if it's non-zero
313 + if (resource->priority > 0) {
314 + buffer_json_member_add_double(mcpc->result, "priority", resource->priority);
315 + }
316 +
317 + buffer_json_object_close(mcpc->result); // Close annotations object
318 + }
319 +
320 + buffer_json_object_close(mcpc->result); // Close resource object
321 + }
322 +
323 + buffer_json_array_close(mcpc->result); // Close resources array
324 + buffer_json_object_close(mcpc->result); // Close result object
325 +
326 + // For now, no need for pagination since we have a small number of resources
327 + // If we add many resources later, implement cursor-based pagination here
328 +
329 + return MCP_RC_OK;
330 +}
331 +
332 +// Implementation of resources/read (transport-agnostic)
333 +static MCP_RETURN_CODE mcp_resources_method_read(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
334 + if (!mcpc || id == 0 || !params) return MCP_RC_INTERNAL_ERROR;
335 +
336 + // Extract URI from params
337 + struct json_object *uri_obj = NULL;
338 + if (!json_object_object_get_ex(params, "uri", &uri_obj)) {
339 + buffer_strcat(mcpc->error, "Missing 'uri' parameter");
340 + return MCP_RC_INVALID_PARAMS;
341 + }
342 +
343 + const char *uri = json_object_get_string(uri_obj);
344 + if (!uri) {
345 + buffer_strcat(mcpc->error, "Invalid 'uri' parameter");
346 + return MCP_RC_INVALID_PARAMS;
347 + }
348 +
349 + netdata_log_debug(D_MCP, "MCP resources/read for URI: %s", uri);
350 +
351 + // Find the matching resource in our array
352 + for (size_t i = 0; i < _countof(mcp_resources); i++) {
353 + const MCP_RESOURCE *resource = &mcp_resources[i];
354 +
355 + // Get the URI without query parameters for matching
356 + const char *query_start = strchr(uri, '?');
357 + size_t base_uri_length = query_start ? (size_t)(query_start - uri) : strlen(uri);
358 +
359 + // Check if the base URI matches the resource URI
360 + if (strlen(resource->uri) == base_uri_length &&
361 + strncmp(resource->uri, uri, base_uri_length) == 0) {
362 +
363 + // Found matching resource, check if read function exists
364 + if (resource->read_fn) {
365 + // Call the resource-specific read function
366 + return resource->read_fn(mcpc, params, id);
367 + }
368 + else {
369 + // No read function implemented
370 + buffer_strcat(mcpc->error, "Resource reading not implemented");
371 + return MCP_RC_NOT_IMPLEMENTED;
372 + }
373 + }
374 + }
375 +
376 + // No matching resource found
377 + buffer_sprintf(mcpc->error, "Unknown resource URI: %s", uri);
378 + return MCP_RC_NOT_FOUND;
379 +}
380 +
381 +// Implementation of resources/templates/list (transport-agnostic)
382 +static MCP_RETURN_CODE mcp_resources_method_templates_list(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
383 + if (!mcpc || !params || !id) return MCP_RC_INTERNAL_ERROR;
384 +
385 + // Initialize success response
386 + mcp_init_success_result(mcpc, id);
387 +
388 + // Create a resourceTemplates array object
389 + buffer_json_member_add_object(mcpc->result, "result");
390 + buffer_json_member_add_array(mcpc->result, "resourceTemplates");
391 +
392 + // Iterate through our templates array and add each one
393 + for (size_t i = 0; i < _countof(mcp_resource_templates); i++) {
394 + const MCP_RESOURCE_TEMPLATE *template = &mcp_resource_templates[i];
395 +
396 + buffer_json_add_array_item_object(mcpc->result);
397 +
398 + // Add required fields
399 + buffer_json_member_add_string(mcpc->result, "name", template->name);
400 + buffer_json_member_add_string(mcpc->result, "uriTemplate", template->uri_template);
401 +
402 + // Add optional fields
403 + if (template->description) {
404 + buffer_json_member_add_string(mcpc->result, "description", template->description);
405 + }
406 +
407 + // Convert the content_type enum to string
408 + const char *mime_type = content_type_id2string(template->content_type);
409 + if (mime_type) {
410 + buffer_json_member_add_string(mcpc->result, "mimeType", mime_type);
411 + }
412 +
413 + // Add audience annotations if specified
414 + if (template->audience != 0) {
415 + buffer_json_member_add_object(mcpc->result, "annotations");
416 +
417 + buffer_json_member_add_array(mcpc->result, "audience");
418 +
419 + if (template->audience & RESOURCE_AUDIENCE_USER) {
420 + buffer_json_add_array_item_string(mcpc->result, "user");
421 + }
422 +
423 + if (template->audience & RESOURCE_AUDIENCE_ASSISTANT) {
424 + buffer_json_add_array_item_string(mcpc->result, "assistant");
425 + }
426 +
427 + buffer_json_array_close(mcpc->result); // Close audience array
428 +
429 + // Add priority if it's non-zero
430 + if (template->priority > 0) {
431 + buffer_json_member_add_double(mcpc->result, "priority", template->priority);
432 + }
433 +
434 + buffer_json_object_close(mcpc->result); // Close annotations object
435 + }
436 +
437 + buffer_json_object_close(mcpc->result); // Close template object
438 + }
439 +
440 + buffer_json_array_close(mcpc->result); // Close resourceTemplates array
441 + buffer_json_object_close(mcpc->result); // Close result object
442 + buffer_json_finalize(mcpc->result);
443 +
444 + // For now, no need for pagination since we have a small number of templates
445 + // If we add many templates later, implement cursor-based pagination here
446 +
447 + return MCP_RC_OK;
448 +}
449 +
450 +// Implementation of resources/subscribe (transport-agnostic)
451 +static MCP_RETURN_CODE mcp_resources_method_subscribe(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
452 + if (!mcpc || !id || !params) return MCP_RC_INTERNAL_ERROR;
453 + return MCP_RC_NOT_IMPLEMENTED;
454 +}
455 +
456 +// Implementation of resources/unsubscribe (transport-agnostic)
457 +static MCP_RETURN_CODE mcp_resources_method_unsubscribe(MCP_CLIENT *mcpc, struct json_object *params, uint64_t id) {
458 + if (!mcpc || id == 0 || !params) return MCP_RC_INTERNAL_ERROR;
459 + return MCP_RC_NOT_IMPLEMENTED;
460 +}
461 +
462 +// Resources namespace method dispatcher (transport-agnostic)
463 +MCP_RETURN_CODE mcp_resources_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id) {
464 + if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
465 +
466 + netdata_log_debug(D_MCP, "MCP resources method: %s", method);
467 +
468 + // Clear previous buffers
469 + buffer_flush(mcpc->result);
470 + buffer_flush(mcpc->error);
471 +
472 + MCP_RETURN_CODE rc;
473 +
474 + if (strcmp(method, "list") == 0) {
475 + rc = mcp_resources_method_list(mcpc, params, id);
476 + }
477 + else if (strcmp(method, "read") == 0) {
478 + rc = mcp_resources_method_read(mcpc, params, id);
479 + }
480 + else if (strcmp(method, "templates/list") == 0) {
481 + rc = mcp_resources_method_templates_list(mcpc, params, id);
482 + }
483 + else if (strcmp(method, "subscribe") == 0) {
484 + rc = mcp_resources_method_subscribe(mcpc, params, id);
485 + }
486 + else if (strcmp(method, "unsubscribe") == 0) {
487 + rc = mcp_resources_method_unsubscribe(mcpc, params, id);
488 + }
489 + else {
490 + // Method not found in resources namespace
491 + buffer_sprintf(mcpc->error, "Method 'resources/%s' not implemented yet", method);
492 + rc = MCP_RC_NOT_IMPLEMENTED;
493 + }
494 +
495 + return rc;
496 +}
src/web/mcp/mcp-resources.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_RESOURCES_H
4 +#define NETDATA_MCP_RESOURCES_H
5 +
6 +#include "mcp.h"
7 +
8 +// Resources namespace method dispatcher (transport-agnostic)
9 +MCP_RETURN_CODE mcp_resources_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_RESOURCES_H
\ No newline at end of file
src/web/mcp/mcp-system.c new
+114
@@ -0,0 +1,114 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +/**
4 + * MCP System Namespace
5 + *
6 + * The MCP System namespace provides methods for querying and managing the server system.
7 + * These methods provide information about the server's state, health, and performance,
8 + * and allow for basic administrative operations.
9 + *
10 + * Key features of the system namespace:
11 + *
12 + * 1. System Information:
13 + * - Get server health status (system/health)
14 + * - Get detailed version information (system/version)
15 + * - Get server performance metrics (system/metrics)
16 + * - Get current system status (system/status)
17 + *
18 + * 2. System Management:
19 + * - Request server restart (system/restart)
20 + *
21 + * System methods typically require elevated permissions, as they can affect
22 + * the operation of the server and may provide sensitive information.
23 + *
24 + * In the Netdata context, system methods provide:
25 + * - Netdata Agent version and build information
26 + * - Server metrics (CPU, memory usage, uptime, etc.)
27 + * - Runtime configuration status
28 + * - Agent health and operational status
29 + * - Administrative operations for authorized users
30 + *
31 + * These methods are particularly useful for:
32 + * - System administrators monitoring Netdata servers
33 + * - Tools that need to check for version compatibility
34 + * - Health monitoring systems tracking Netdata itself
35 + * - Administrative interfaces
36 + */
37 +
38 +#include "mcp-system.h"
39 +#include "mcp-initialize.h"
40 +#include "config.h" // Include config.h for NETDATA_VERSION
41 +
42 +// Stub implementations for system methods (transport-agnostic)
43 +static MCP_RETURN_CODE mcp_system_method_health(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
44 + buffer_sprintf(mcpc->error, "Method 'system/health' not implemented yet");
45 + return MCP_RC_NOT_IMPLEMENTED;
46 +}
47 +
48 +static MCP_RETURN_CODE mcp_system_method_version(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
49 + if (!mcpc || id == 0) return MCP_RC_ERROR;
50 +
51 + // Initialize success response
52 + mcp_init_success_result(mcpc, id);
53 +
54 + // Add version information
55 + buffer_json_member_add_string(mcpc->result, "name", "Netdata");
56 + buffer_json_member_add_string(mcpc->result, "version", NETDATA_VERSION);
57 + buffer_json_member_add_string(mcpc->result, "mcpVersion", MCP_PROTOCOL_VERSION_2str(MCP_PROTOCOL_VERSION_LATEST));
58 +
59 + // Close the result object
60 + buffer_json_finalize(mcpc->result);
61 +
62 + return MCP_RC_OK;
63 +}
64 +
65 +static MCP_RETURN_CODE mcp_system_method_metrics(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
66 + buffer_sprintf(mcpc->error, "Method 'system/metrics' not implemented yet");
67 + return MCP_RC_NOT_IMPLEMENTED;
68 +}
69 +
70 +static MCP_RETURN_CODE mcp_system_method_restart(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
71 + buffer_sprintf(mcpc->error, "Method 'system/restart' not implemented yet");
72 + return MCP_RC_NOT_IMPLEMENTED;
73 +}
74 +
75 +static MCP_RETURN_CODE mcp_system_method_status(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
76 + buffer_sprintf(mcpc->error, "Method 'system/status' not implemented yet");
77 + return MCP_RC_NOT_IMPLEMENTED;
78 +}
79 +
80 +// System namespace method dispatcher (transport-agnostic)
81 +MCP_RETURN_CODE mcp_system_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id) {
82 + if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
83 +
84 + netdata_log_debug(D_MCP, "MCP system method: %s", method);
85 +
86 + // Flush previous buffers
87 + buffer_flush(mcpc->result);
88 + buffer_flush(mcpc->error);
89 +
90 + MCP_RETURN_CODE rc;
91 +
92 + if (strcmp(method, "health") == 0) {
93 + rc = mcp_system_method_health(mcpc, params, id);
94 + }
95 + else if (strcmp(method, "version") == 0) {
96 + rc = mcp_system_method_version(mcpc, params, id);
97 + }
98 + else if (strcmp(method, "metrics") == 0) {
99 + rc = mcp_system_method_metrics(mcpc, params, id);
100 + }
101 + else if (strcmp(method, "restart") == 0) {
102 + rc = mcp_system_method_restart(mcpc, params, id);
103 + }
104 + else if (strcmp(method, "status") == 0) {
105 + rc = mcp_system_method_status(mcpc, params, id);
106 + }
107 + else {
108 + // Method not found in system namespace
109 + buffer_sprintf(mcpc->error, "Method 'system/%s' not implemented yet", method);
110 + rc = MCP_RC_NOT_IMPLEMENTED;
111 + }
112 +
113 + return rc;
114 +}
src/web/mcp/mcp-system.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_SYSTEM_H
4 +#define NETDATA_MCP_SYSTEM_H
5 +
6 +#include "mcp.h"
7 +
8 +// System namespace method dispatcher (transport-agnostic)
9 +MCP_RETURN_CODE mcp_system_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_SYSTEM_H
\ No newline at end of file
src/web/mcp/mcp-tools.c new
+218
@@ -0,0 +1,218 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +/**
4 + * MCP Tools Namespace
5 + *
6 + * The MCP Tools namespace provides methods for discovering and executing tools offered by the server.
7 + * In the MCP protocol, tools are discrete operations that clients can invoke to perform specific actions.
8 + *
9 + * Tools are model-controlled actions - meaning the AI decides when and how to use them based on context.
10 + * Each tool has a defined input schema that specifies required and optional parameters.
11 + *
12 + * Key features of the tools namespace:
13 + *
14 + * 1. Tool Discovery:
15 + * - Clients can list available tools (tools/list)
16 + * - Get detailed descriptions of specific tools (tools/describe)
17 + * - Understand what parameters a tool requires (through JSON Schema)
18 + *
19 + * 2. Tool Execution:
20 + * - Execute tools with specific parameters (tools/execute)
21 + * - Validate parameters without execution (tools/validate)
22 + * - Asynchronous execution is supported for long-running tools
23 + *
24 + * 3. Execution Management:
25 + * - Check execution status (tools/status)
26 + * - Cancel running executions (tools/cancel)
27 + *
28 + * In the Netdata context, tools provide access to operations like:
29 + * - Exploring metrics and their relationships
30 + * - Analyzing time-series data patterns
31 + * - Finding correlations between metrics
32 + * - Root cause analysis for anomalies
33 + * - Summarizing system health
34 + *
35 + * Each tool execution is assigned a unique ID, allowing clients to track and manage executions.
36 + */
37 +
38 +#include "mcp-tools.h"
39 +#include "mcp-initialize.h"
40 +
41 +// Return a list of available tools (transport-agnostic)
42 +static MCP_RETURN_CODE mcp_tools_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
43 + if (!mcpc || id == 0) return MCP_RC_ERROR;
44 +
45 + // Initialize success response
46 + mcp_init_success_result(mcpc, id);
47 +
48 + // Create tools array
49 + buffer_json_member_add_array(mcpc->result, "tools");
50 +
51 + // Add explore_metrics tool
52 + buffer_json_add_array_item_object(mcpc->result);
53 + buffer_json_member_add_string(mcpc->result, "name", "explore_metrics");
54 + buffer_json_member_add_string(mcpc->result, "description",
55 + "Explore Netdata's time-series metrics with support for high-resolution data");
56 +
57 + // Add input schema for metrics tool
58 + buffer_json_member_add_object(mcpc->result, "inputSchema");
59 + buffer_json_member_add_string(mcpc->result, "type", "object");
60 + buffer_json_member_add_string(mcpc->result, "title", "MetricsQuery");
61 +
62 + // Properties
63 + buffer_json_member_add_object(mcpc->result, "properties");
64 +
65 + // Context property
66 + buffer_json_member_add_object(mcpc->result, "context");
67 + buffer_json_member_add_string(mcpc->result, "type", "string");
68 + buffer_json_member_add_string(mcpc->result, "title", "Context");
69 + buffer_json_object_close(mcpc->result); // Close context
70 +
71 + // After property
72 + buffer_json_member_add_object(mcpc->result, "after");
73 + buffer_json_member_add_string(mcpc->result, "type", "integer");
74 + buffer_json_member_add_string(mcpc->result, "title", "After");
75 + buffer_json_object_close(mcpc->result); // Close after
76 +
77 + // Before property
78 + buffer_json_member_add_object(mcpc->result, "before");
79 + buffer_json_member_add_string(mcpc->result, "type", "integer");
80 + buffer_json_member_add_string(mcpc->result, "title", "Before");
81 + buffer_json_object_close(mcpc->result); // Close before
82 +
83 + // Points property
84 + buffer_json_member_add_object(mcpc->result, "points");
85 + buffer_json_member_add_string(mcpc->result, "type", "integer");
86 + buffer_json_member_add_string(mcpc->result, "title", "Points");
87 + buffer_json_object_close(mcpc->result); // Close points
88 +
89 + // Group property
90 + buffer_json_member_add_object(mcpc->result, "group");
91 + buffer_json_member_add_string(mcpc->result, "type", "string");
92 + buffer_json_member_add_string(mcpc->result, "title", "Group");
93 + buffer_json_object_close(mcpc->result); // Close group
94 +
95 + buffer_json_object_close(mcpc->result); // Close properties
96 +
97 + // Required properties
98 + buffer_json_member_add_array(mcpc->result, "required");
99 + buffer_json_add_array_item_string(mcpc->result, "context");
100 + buffer_json_array_close(mcpc->result); // Close required
101 +
102 + buffer_json_object_close(mcpc->result); // Close inputSchema
103 + buffer_json_object_close(mcpc->result); // Close explore_metrics tool
104 +
105 + // Add explore_nodes tool
106 + buffer_json_add_array_item_object(mcpc->result);
107 + buffer_json_member_add_string(mcpc->result, "name", "explore_nodes");
108 + buffer_json_member_add_string(mcpc->result, "description",
109 + "Discover and explore all monitored nodes in your infrastructure");
110 +
111 + // Add input schema for nodes tool
112 + buffer_json_member_add_object(mcpc->result, "inputSchema");
113 + buffer_json_member_add_string(mcpc->result, "type", "object");
114 + buffer_json_member_add_string(mcpc->result, "title", "NodesQuery");
115 +
116 + // Properties
117 + buffer_json_member_add_object(mcpc->result, "properties");
118 +
119 + // Filter property
120 + buffer_json_member_add_object(mcpc->result, "filter");
121 + buffer_json_member_add_string(mcpc->result, "type", "string");
122 + buffer_json_member_add_string(mcpc->result, "title", "Filter");
123 + buffer_json_object_close(mcpc->result); // Close filter
124 +
125 + buffer_json_object_close(mcpc->result); // Close properties
126 + buffer_json_object_close(mcpc->result); // Close inputSchema
127 + buffer_json_object_close(mcpc->result); // Close explore_nodes tool
128 +
129 + buffer_json_array_close(mcpc->result); // Close tools array
130 + buffer_json_finalize(mcpc->result); // Finalize the JSON
131 +
132 + return MCP_RC_OK;
133 +}
134 +
135 +// Stub implementations for other tools methods (transport-agnostic)
136 +static MCP_RETURN_CODE mcp_tools_method_execute(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
137 + buffer_sprintf(mcpc->error, "Method 'tools/execute' not implemented yet");
138 + return MCP_RC_NOT_IMPLEMENTED;
139 +}
140 +
141 +static MCP_RETURN_CODE mcp_tools_method_cancel(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
142 + buffer_sprintf(mcpc->error, "Method 'tools/cancel' not implemented yet");
143 + return MCP_RC_NOT_IMPLEMENTED;
144 +}
145 +
146 +static MCP_RETURN_CODE mcp_tools_method_status(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
147 + buffer_sprintf(mcpc->error, "Method 'tools/status' not implemented yet");
148 + return MCP_RC_NOT_IMPLEMENTED;
149 +}
150 +
151 +static MCP_RETURN_CODE mcp_tools_method_validate(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
152 + buffer_sprintf(mcpc->error, "Method 'tools/validate' not implemented yet");
153 + return MCP_RC_NOT_IMPLEMENTED;
154 +}
155 +
156 +static MCP_RETURN_CODE mcp_tools_method_describe(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id __maybe_unused) {
157 + buffer_sprintf(mcpc->error, "Method 'tools/describe' not implemented yet");
158 + return MCP_RC_NOT_IMPLEMENTED;
159 +}
160 +
161 +static MCP_RETURN_CODE mcp_tools_method_getCapabilities(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, uint64_t id) {
162 + if (!mcpc || id == 0) return MCP_RC_ERROR;
163 +
164 + // Initialize success response
165 + mcp_init_success_result(mcpc, id);
166 +
167 + // Add capabilities as result object properties
168 + buffer_json_member_add_boolean(mcpc->result, "listChanged", false);
169 + buffer_json_member_add_boolean(mcpc->result, "asyncExecution", true);
170 + buffer_json_member_add_boolean(mcpc->result, "batchExecution", true);
171 +
172 + // Close the result object
173 + buffer_json_finalize(mcpc->result);
174 +
175 + return MCP_RC_OK;
176 +}
177 +
178 +// Tools namespace method dispatcher (transport-agnostic)
179 +MCP_RETURN_CODE mcp_tools_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id) {
180 + if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
181 +
182 + netdata_log_debug(D_MCP, "MCP tools method: %s", method);
183 +
184 + // Flush previous buffers
185 + buffer_flush(mcpc->result);
186 + buffer_flush(mcpc->error);
187 +
188 + MCP_RETURN_CODE rc;
189 +
190 + if (strcmp(method, "list") == 0) {
191 + rc = mcp_tools_method_list(mcpc, params, id);
192 + }
193 + else if (strcmp(method, "execute") == 0) {
194 + rc = mcp_tools_method_execute(mcpc, params, id);
195 + }
196 + else if (strcmp(method, "cancel") == 0) {
197 + rc = mcp_tools_method_cancel(mcpc, params, id);
198 + }
199 + else if (strcmp(method, "status") == 0) {
200 + rc = mcp_tools_method_status(mcpc, params, id);
201 + }
202 + else if (strcmp(method, "validate") == 0) {
203 + rc = mcp_tools_method_validate(mcpc, params, id);
204 + }
205 + else if (strcmp(method, "describe") == 0) {
206 + rc = mcp_tools_method_describe(mcpc, params, id);
207 + }
208 + else if (strcmp(method, "getCapabilities") == 0) {
209 + rc = mcp_tools_method_getCapabilities(mcpc, params, id);
210 + }
211 + else {
212 + // Method not found in tools namespace
213 + buffer_sprintf(mcpc->error, "Method 'tools/%s' not implemented yet", method);
214 + rc = MCP_RC_NOT_IMPLEMENTED;
215 + }
216 +
217 + return rc;
218 +}
src/web/mcp/mcp-tools.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_TOOLS_H
4 +#define NETDATA_MCP_TOOLS_H
5 +
6 +#include "mcp.h"
7 +
8 +// Tools namespace method dispatcher (transport-agnostic)
9 +MCP_RETURN_CODE mcp_tools_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, uint64_t id);
10 +
11 +#endif // NETDATA_MCP_TOOLS_H
\ No newline at end of file
src/web/mcp/mcp-websocket-test.html new
+1272
@@ -0,0 +1,1272 @@
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 + <title>Netdata MCP WebSocket Test</title>
7 + <style>
8 + body {
9 + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10 + margin: 0;
11 + padding: 10px;
12 + line-height: 1.6;
13 + color: #333;
14 + max-width: 100%;
15 + box-sizing: border-box;
16 + overflow: hidden;
17 + }
18 + h1 {
19 + color: #0088cc;
20 + margin-top: 0;
21 + margin-bottom: 10px;
22 + }
23 + .connection-panel {
24 + background-color: #e2f4ff;
25 + padding: 8px;
26 + border-radius: 5px;
27 + margin-bottom: 10px;
28 + display: flex;
29 + align-items: center;
30 + flex-wrap: wrap;
31 + gap: 8px;
32 + }
33 + .four-column-layout {
34 + display: grid;
35 + grid-template-columns: 195px 260px 1fr 2fr;
36 + gap: 10px;
37 + height: calc(100vh - 120px);
38 + min-height: 500px;
39 + }
40 + .column {
41 + background-color: #f5f5f5;
42 + border-radius: 5px;
43 + padding: 10px;
44 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
45 + display: flex;
46 + flex-direction: column;
47 + overflow: hidden;
48 + position: relative; /* Added for absolute positioning of buttons */
49 + }
50 + .column-header {
51 + margin-top: 0;
52 + margin-bottom: 10px;
53 + padding-bottom: 8px;
54 + border-bottom: 1px solid #ddd;
55 + font-size: 1.2em;
56 + color: #0088cc;
57 + display: flex;
58 + justify-content: space-between;
59 + align-items: center;
60 + }
61 + .column-content {
62 + overflow-y: auto;
63 + flex: 1;
64 + }
65 + .flow-item, .method-item {
66 + padding: 8px;
67 + margin-bottom: 6px;
68 + border-radius: 5px;
69 + cursor: pointer;
70 + transition: background-color 0.2s;
71 + }
72 + .flow-item:hover, .method-item:hover {
73 + background-color: #e0e0e0;
74 + }
75 + .flow-item.active, .method-item.active {
76 + background-color: #d0e8f2;
77 + font-weight: bold;
78 + }
79 + #jsonEditor {
80 + width: 100%;
81 + height: 100%;
82 + font-family: monospace;
83 + font-size: 14px;
84 + padding: 8px;
85 + border: 1px solid #ccc;
86 + border-radius: 5px;
87 + resize: none;
88 + box-sizing: border-box;
89 + }
90 + /* JSON syntax highlighting */
91 + .json-string {
92 + color: #008800;
93 + font-weight: bold;
94 + display: inline-block; /* Allow multiline strings to format properly */
95 + }
96 + .json-number { color: #0055aa; font-weight: bold; }
97 + .json-boolean-true { color: #008800; font-weight: bold; }
98 + .json-boolean-false { color: #dd0000; font-weight: bold; }
99 + .json-null { color: #666666; font-weight: bold; }
100 + .json-key { color: #555555; }
101 + .json-bracket { color: #aaaaaa; }
102 + .json-comma { color: #aaaaaa; }
103 + .json-colon { color: #aaaaaa; }
104 + #requestData {
105 + background-color: #f0f0f0;
106 + padding: 10px;
107 + border-radius: 5px;
108 + margin-top: 10px;
109 + font-family: monospace;
110 + font-size: 12px;
111 + overflow: auto;
112 + white-space: pre-wrap;
113 + max-height: 100px;
114 + }
115 + button {
116 + background-color: #0088cc;
117 + color: white;
118 + border: none;
119 + padding: 8px 12px;
120 + border-radius: 5px;
121 + cursor: pointer;
122 + font-size: 14px;
123 + }
124 + button:hover {
125 + background-color: #006699;
126 + }
127 + button:disabled {
128 + background-color: #cccccc;
129 + cursor: not-allowed;
130 + }
131 + .button-row {
132 + display: flex;
133 + gap: 8px;
134 + margin-top: 8px;
135 + }
136 + .status {
137 + font-weight: bold;
138 + }
139 + .connected {
140 + color: green;
141 + }
142 + .disconnected {
143 + color: red;
144 + }
145 + .tooltip {
146 + position: relative;
147 + display: inline-block;
148 + cursor: help;
149 + }
150 + .tooltip .tooltiptext {
151 + visibility: hidden;
152 + width: 300px;
153 + background-color: #555;
154 + color: #fff;
155 + text-align: left;
156 + border-radius: 6px;
157 + padding: 10px;
158 + position: absolute;
159 + z-index: 1;
160 + bottom: 125%;
161 + left: 50%;
162 + transform: translateX(-50%);
163 + opacity: 0;
164 + transition: opacity 0.3s;
165 + font-size: 12px;
166 + line-height: 1.4;
167 + }
168 + .tooltip:hover .tooltiptext {
169 + visibility: visible;
170 + opacity: 1;
171 + }
172 +
173 + /* Ensure JSON content wraps properly */
174 + pre, #responseViewer, #requestData {
175 + white-space: pre-wrap !important;
176 + word-break: break-word !important;
177 + overflow-x: auto !important;
178 + }
179 + </style>
180 +</head>
181 +<body>
182 + <h1>Netdata MCP WebSocket Test</h1>
183 +
184 + <div class="connection-panel">
185 + <label for="serverUrl">WebSocket Server URL:</label>
186 + <input type="text" id="serverUrl" value="ws://localhost:19999/mcp" style="width: 300px;">
187 + <button id="connectBtn">Connect</button>
188 + <button id="disconnectBtn" disabled>Disconnect</button>
189 + <span id="status" class="status disconnected">Disconnected</span>
190 + </div>
191 +
192 + <div class="four-column-layout">
193 + <!-- First Column - Flows -->
194 + <div class="column">
195 + <h3 class="column-header">Flows</h3>
196 + <div class="column-content" id="flowsContainer">
197 + <div class="flow-item active" data-flow="initialize-flow">Initialize</div>
198 + <div class="flow-item" data-flow="tools-flow">Tools</div>
199 + <div class="flow-item" data-flow="resources-flow">Resources</div>
200 + <div class="flow-item" data-flow="prompts-flow">Prompts</div>
201 + <div class="flow-item" data-flow="notifications-flow">Notifications</div>
202 + <div class="flow-item" data-flow="context-flow">Context</div>
203 + <div class="flow-item" data-flow="system-flow">System</div>
204 + </div>
205 + </div>
206 +
207 + <!-- Second Column - Methods -->
208 + <div class="column">
209 + <h3 class="column-header">Methods</h3>
210 + <div class="column-content" id="methodsContainer">
211 + <!-- Methods will be loaded here -->
212 + </div>
213 + </div>
214 +
215 + <!-- Third Column - Request Editor -->
216 + <div class="column">
217 + <h3 class="column-header">
218 + Request
219 + <span class="tooltip">ⓘ
220 + <span class="tooltiptext">
221 + JSON-RPC 2.0 request format requires "jsonrpc": "2.0", "method" and an optional "id".
222 + </span>
223 + </span>
224 + </h3>
225 + <div style="display: flex; flex-direction: column; height: 100%;">
226 + <!-- 50% height for JSON editor -->
227 + <textarea id="jsonEditor" style="height: 50%; min-height: 200px; margin-bottom: 8px; resize: none;"></textarea>
228 +
229 + <!-- Log container with remaining height minus buttons -->
230 + <div style="display: flex; flex-direction: column; height: calc(50% - 60px); margin-bottom: 50px;">
231 + <div id="requestData" style="background-color: #f0f0f0; padding: 6px; border-radius: 4px; margin-bottom: 4px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-word; height: 60px; overflow: auto;"></div>
232 + <pre id="commLog" style="flex: 1; background-color: #f8f8f8; border: 1px solid #ddd; border-radius: 4px; padding: 6px; font-family: monospace; font-size: 12px; overflow: auto; margin: 0;"></pre>
233 + </div>
234 +
235 + <!-- Fixed position buttons at the bottom -->
236 + <div class="button-row" style="position: absolute; bottom: 10px; left: 10px; right: 10px; background: #f5f5f5; padding: 5px 0;">
237 + <button id="sendBtn" disabled>Send</button>
238 + <button id="resetBtn">Reset</button>
239 + <button id="clearLogBtn">Clear Log</button>
240 + </div>
241 + </div>
242 + </div>
243 +
244 + <!-- Fourth Column - Response Viewer -->
245 + <div class="column">
246 + <h3 class="column-header">Response</h3>
247 + <div style="display: flex; flex-direction: column; height: calc(100% - 35px);">
248 + <pre id="responseViewer" style="flex: 1; overflow: auto; margin: 0; padding: 6px; background-color: #f8f8f8; border: 1px solid #ddd; border-radius: 4px; white-space: pre-wrap; word-break: break-word;"><span class="json-null">No response yet.</span></pre>
249 + </div>
250 + </div>
251 + </div>
252 +
253 + <script>
254 + // DOM elements
255 + const serverUrlInput = document.getElementById('serverUrl');
256 + const connectBtn = document.getElementById('connectBtn');
257 + const disconnectBtn = document.getElementById('disconnectBtn');
258 + const statusSpan = document.getElementById('status');
259 + const jsonEditor = document.getElementById('jsonEditor');
260 + const sendBtn = document.getElementById('sendBtn');
261 + const resetBtn = document.getElementById('resetBtn');
262 + const clearLogBtn = document.getElementById('clearLogBtn');
263 + const responseViewer = document.getElementById('responseViewer');
264 + const requestData = document.getElementById('requestData');
265 + const commLog = document.getElementById('commLog');
266 + const flowsContainer = document.getElementById('flowsContainer');
267 + const methodsContainer = document.getElementById('methodsContainer');
268 +
269 + // WebSocket connection
270 + let socket = null;
271 + let requestId = 1;
272 + let isConnected = false;
273 + let requestTimestamp = null;
274 +
275 + // Function to highlight JSON syntax with colors
276 + function syntaxHighlightJson(json) {
277 + // Use more space for indentation to make it easier to read
278 + const jsonStr = JSON.stringify(json, null, 4);
279 +
280 + // A more direct approach for syntax highlighting
281 + let result = '';
282 + let inString = false;
283 + let isKey = false;
284 + let currentStr = '';
285 + let lastChar = '';
286 +
287 + // Process each character individually to maintain proper context
288 + for (let i = 0; i < jsonStr.length; i++) {
289 + const char = jsonStr[i];
290 +
291 + // Handle string delimiters
292 + if (char === '"' && lastChar !== '\\') {
293 + if (!inString) {
294 + // Start of a new string
295 + inString = true;
296 + currentStr = char;
297 +
298 + // Check if this might be a key by looking ahead for a colon
299 + isKey = false;
300 + for (let j = i + 1; j < jsonStr.length; j++) {
301 + if (jsonStr[j] === '"' && jsonStr[j-1] !== '\\') {
302 + // End of string found, now look for colon
303 + for (let k = j + 1; k < jsonStr.length; k++) {
304 + if (jsonStr[k] === ':') {
305 + isKey = true;
306 + break;
307 + } else if (jsonStr[k] !== ' ' && jsonStr[k] !== '\t' && jsonStr[k] !== '\n') {
308 + break;
309 + }
310 + }
311 + break;
312 + }
313 + }
314 + } else {
315 + // End of a string
316 + currentStr += char;
317 + inString = false;
318 +
319 + // Apply the appropriate class for string formatting
320 + if (isKey) {
321 + result += '<span class="json-key">' + escapeHtml(currentStr) + '</span>';
322 + } else {
323 + result += '<span class="json-string">' + escapeHtml(currentStr) + '</span>';
324 + }
325 + currentStr = '';
326 + }
327 + }
328 + else if (inString) {
329 + // Inside a string, collect characters
330 + currentStr += char;
331 + }
332 + else if (char === ':') {
333 + result += '<span class="json-colon">:</span>';
334 + }
335 + else if (char === ',') {
336 + result += '<span class="json-comma">,</span>';
337 + }
338 + else if (char === '{' || char === '}' || char === '[' || char === ']') {
339 + result += '<span class="json-bracket">' + char + '</span>';
340 + }
341 + else if (char === 't' && jsonStr.substring(i, i+4) === 'true') {
342 + result += '<span class="json-boolean-true">true</span>';
343 + i += 3; // Skip the rest of "true"
344 + }
345 + else if (char === 'f' && jsonStr.substring(i, i+5) === 'false') {
346 + result += '<span class="json-boolean-false">false</span>';
347 + i += 4; // Skip the rest of "false"
348 + }
349 + else if (char === 'n' && jsonStr.substring(i, i+4) === 'null') {
350 + result += '<span class="json-null">null</span>';
351 + i += 3; // Skip the rest of "null"
352 + }
353 + else if (/[0-9]/.test(char) || char === '-') {
354 + // Start of a number
355 + let numberStr = char;
356 + for (let j = i + 1; j < jsonStr.length; j++) {
357 + if (/[0-9.eE+-]/.test(jsonStr[j])) {
358 + numberStr += jsonStr[j];
359 + i++;
360 + } else {
361 + break;
362 + }
363 + }
364 + result += '<span class="json-number">' + numberStr + '</span>';
365 + }
366 + else {
367 + // Whitespace and other characters
368 + result += char;
369 + }
370 +
371 + lastChar = char;
372 + }
373 +
374 + return result;
375 + }
376 +
377 + // Helper function to escape HTML entities
378 + function escapeHtml(text) {
379 + return text
380 + .replace(/&/g, '&amp;')
381 + .replace(/</g, '&lt;')
382 + .replace(/>/g, '&gt;')
383 + // Replace literal \n with actual line breaks for display
384 + .replace(/\\n/g, '<br>');
385 + }
386 +
387 + // State storage for method requests and responses
388 + const methodState = new Map();
389 + let currentFlow = 'initialize-flow';
390 + let currentMethod = null;
391 +
392 + // Method definitions by flow
393 + const flows = {
394 + 'initialize-flow': {
395 + name: 'Initialize',
396 + methods: {
397 + 'initialize': {
398 + name: 'initialize',
399 + description: 'Get information about available resources and methods',
400 + defaultRequest: {
401 + jsonrpc: '2.0',
402 + method: 'initialize',
403 + params: {
404 + protocolVersion: '2024-11-05',
405 + capabilities: {
406 + textDocument: {
407 + synchronization: {
408 + incremental: true
409 + }
410 + },
411 + completion: {
412 + contextSupport: true
413 + }
414 + },
415 + clientInfo: {
416 + name: 'netdata-mcp-test',
417 + version: '1.0.0',
418 + description: 'Netdata MCP Protocol Test Client'
419 + }
420 + },
421 + id: 1
422 + }
423 + }
424 + }
425 + },
426 + 'tools-flow': {
427 + name: 'Tools',
428 + methods: {
429 + 'tools/list': {
430 + name: 'tools/list',
431 + description: 'Get list of available tools with their schemas',
432 + defaultRequest: {
433 + jsonrpc: '2.0',
434 + method: 'tools/list',
435 + params: {},
436 + id: 1
437 + }
438 + },
439 + 'tools/execute': {
440 + name: 'tools/execute',
441 + description: 'Execute a tool with parameters',
442 + defaultRequest: {
443 + jsonrpc: '2.0',
444 + method: 'tools/execute',
445 + params: {
446 + name: 'explore_metrics',
447 + input: {
448 + context: 'system.cpu'
449 + }
450 + },
451 + id: 1
452 + }
453 + },
454 + 'tools/cancel': {
455 + name: 'tools/cancel',
456 + description: 'Cancel a running tool execution',
457 + defaultRequest: {
458 + jsonrpc: '2.0',
459 + method: 'tools/cancel',
460 + params: {
461 + executionId: '12345'
462 + },
463 + id: 1
464 + }
465 + },
466 + 'tools/status': {
467 + name: 'tools/status',
468 + description: 'Get status of a tool execution',
469 + defaultRequest: {
470 + jsonrpc: '2.0',
471 + method: 'tools/status',
472 + params: {
473 + executionId: '12345'
474 + },
475 + id: 1
476 + }
477 + },
478 + 'tools/validate': {
479 + name: 'tools/validate',
480 + description: 'Validate parameters for a tool',
481 + defaultRequest: {
482 + jsonrpc: '2.0',
483 + method: 'tools/validate',
484 + params: {
485 + name: 'explore_metrics',
486 + input: {
487 + context: 'system.cpu'
488 + }
489 + },
490 + id: 1
491 + }
492 + },
493 + 'tools/describe': {
494 + name: 'tools/describe',
495 + description: 'Get detailed documentation for a tool',
496 + defaultRequest: {
497 + jsonrpc: '2.0',
498 + method: 'tools/describe',
499 + params: {
500 + name: 'explore_metrics'
501 + },
502 + id: 1
503 + }
504 + },
505 + 'tools/getCapabilities': {
506 + name: 'tools/getCapabilities',
507 + description: 'Get capabilities of the tools subsystem',
508 + defaultRequest: {
509 + jsonrpc: '2.0',
510 + method: 'tools/getCapabilities',
511 + params: {},
512 + id: 1
513 + }
514 + }
515 + }
516 + },
517 + 'resources-flow': {
518 + name: 'Resources',
519 + methods: {
520 + 'resources/list': {
521 + name: 'resources/list',
522 + description: 'Get list of available resources',
523 + defaultRequest: {
524 + jsonrpc: '2.0',
525 + method: 'resources/list',
526 + params: {
527 + // Optional pagination cursor
528 + // cursor: "pagination_token"
529 + },
530 + id: 1
531 + }
532 + },
533 + 'resources/read': {
534 + name: 'resources/read',
535 + description: 'Read a specific resource by URI',
536 + defaultRequest: {
537 + jsonrpc: '2.0',
538 + method: 'resources/read',
539 + params: {
540 + uri: 'nd://contexts'
541 + },
542 + id: 1
543 + }
544 + },
545 + 'resources/templates/list': {
546 + name: 'resources/templates/list',
547 + description: 'Get list of resource templates for URI construction',
548 + defaultRequest: {
549 + jsonrpc: '2.0',
550 + method: 'resources/templates/list',
551 + params: {
552 + // Optional pagination cursor
553 + // cursor: "pagination_token"
554 + },
555 + id: 1
556 + }
557 + },
558 + 'resources/subscribe': {
559 + name: 'resources/subscribe',
560 + description: 'Subscribe to changes in a resource',
561 + defaultRequest: {
562 + jsonrpc: '2.0',
563 + method: 'resources/subscribe',
564 + params: {
565 + uri: 'nd://contexts'
566 + },
567 + id: 1
568 + }
569 + },
570 + 'resources/unsubscribe': {
571 + name: 'resources/unsubscribe',
572 + description: 'Unsubscribe from a resource',
573 + defaultRequest: {
574 + jsonrpc: '2.0',
575 + method: 'resources/unsubscribe',
576 + params: {
577 + uri: 'nd://contexts'
578 + },
579 + id: 1
580 + }
581 + }
582 + }
583 + },
584 + 'prompts-flow': {
585 + name: 'Prompts',
586 + methods: {
587 + 'prompts/list': {
588 + name: 'prompts/list',
589 + description: 'Get list of available prompts',
590 + defaultRequest: {
591 + jsonrpc: '2.0',
592 + method: 'prompts/list',
593 + params: {},
594 + id: 1
595 + }
596 + },
597 + 'prompts/execute': {
598 + name: 'prompts/execute',
599 + description: 'Execute a prompt',
600 + defaultRequest: {
601 + jsonrpc: '2.0',
602 + method: 'prompts/execute',
603 + params: {
604 + name: 'analyze_metrics',
605 + input: {
606 + context: 'system.cpu',
607 + timeframe: '1h'
608 + }
609 + },
610 + id: 1
611 + }
612 + },
613 + 'prompts/get': {
614 + name: 'prompts/get',
615 + description: 'Get a specific prompt',
616 + defaultRequest: {
617 + jsonrpc: '2.0',
618 + method: 'prompts/get',
619 + params: {
620 + name: 'analyze_metrics'
621 + },
622 + id: 1
623 + }
624 + },
625 + 'prompts/save': {
626 + name: 'prompts/save',
627 + description: 'Save a prompt',
628 + defaultRequest: {
629 + jsonrpc: '2.0',
630 + method: 'prompts/save',
631 + params: {
632 + name: 'my_custom_prompt',
633 + description: 'A custom prompt for analysis',
634 + template: 'Analyze the following metrics: {{context}}'
635 + },
636 + id: 1
637 + }
638 + },
639 + 'prompts/delete': {
640 + name: 'prompts/delete',
641 + description: 'Delete a prompt',
642 + defaultRequest: {
643 + jsonrpc: '2.0',
644 + method: 'prompts/delete',
645 + params: {
646 + name: 'my_custom_prompt'
647 + },
648 + id: 1
649 + }
650 + },
651 + 'prompts/getCategories': {
652 + name: 'prompts/getCategories',
653 + description: 'Get categories of prompts',
654 + defaultRequest: {
655 + jsonrpc: '2.0',
656 + method: 'prompts/getCategories',
657 + params: {},
658 + id: 1
659 + }
660 + },
661 + 'prompts/getHistory': {
662 + name: 'prompts/getHistory',
663 + description: 'Get history of prompt executions',
664 + defaultRequest: {
665 + jsonrpc: '2.0',
666 + method: 'prompts/getHistory',
667 + params: {
668 + limit: 10
669 + },
670 + id: 1
671 + }
672 + }
673 + }
674 + },
675 + 'notifications-flow': {
676 + name: 'Notifications',
677 + methods: {
678 + 'notifications/initialized': {
679 + name: 'notifications/initialized',
680 + description: 'Notify server that client is initialized',
681 + defaultRequest: {
682 + jsonrpc: '2.0',
683 + method: 'notifications/initialized'
684 + // No id for notifications
685 + }
686 + },
687 + 'notifications/subscribe': {
688 + name: 'notifications/subscribe',
689 + description: 'Subscribe to notifications',
690 + defaultRequest: {
691 + jsonrpc: '2.0',
692 + method: 'notifications/subscribe',
693 + params: {
694 + type: 'alerts'
695 + },
696 + id: 1
697 + }
698 + },
699 + 'notifications/unsubscribe': {
700 + name: 'notifications/unsubscribe',
701 + description: 'Unsubscribe from notifications',
702 + defaultRequest: {
703 + jsonrpc: '2.0',
704 + method: 'notifications/unsubscribe',
705 + params: {
706 + subscriptionId: '12345'
707 + },
708 + id: 1
709 + }
710 + },
711 + 'notifications/acknowledge': {
712 + name: 'notifications/acknowledge',
713 + description: 'Acknowledge a notification',
714 + defaultRequest: {
715 + jsonrpc: '2.0',
716 + method: 'notifications/acknowledge',
717 + params: {
718 + notificationId: '12345'
719 + },
720 + id: 1
721 + }
722 + },
723 + 'notifications/getHistory': {
724 + name: 'notifications/getHistory',
725 + description: 'Get history of notifications',
726 + defaultRequest: {
727 + jsonrpc: '2.0',
728 + method: 'notifications/getHistory',
729 + params: {
730 + limit: 10
731 + },
732 + id: 1
733 + }
734 + },
735 + 'notifications/send': {
736 + name: 'notifications/send',
737 + description: 'Send a notification',
738 + defaultRequest: {
739 + jsonrpc: '2.0',
740 + method: 'notifications/send',
741 + params: {
742 + type: 'message',
743 + content: 'Test notification from client'
744 + },
745 + id: 1
746 + }
747 + },
748 + 'notifications/getSettings': {
749 + name: 'notifications/getSettings',
750 + description: 'Get notification settings',
751 + defaultRequest: {
752 + jsonrpc: '2.0',
753 + method: 'notifications/getSettings',
754 + params: {},
755 + id: 1
756 + }
757 + }
758 + }
759 + },
760 + 'context-flow': {
761 + name: 'Context',
762 + methods: {
763 + 'context/provide': {
764 + name: 'context/provide',
765 + description: 'Provide context to the server',
766 + defaultRequest: {
767 + jsonrpc: '2.0',
768 + method: 'context/provide',
769 + params: {
770 + context: {
771 + user: 'admin',
772 + preferences: {
773 + theme: 'dark'
774 + }
775 + }
776 + },
777 + id: 1
778 + }
779 + },
780 + 'context/clear': {
781 + name: 'context/clear',
782 + description: 'Clear context',
783 + defaultRequest: {
784 + jsonrpc: '2.0',
785 + method: 'context/clear',
786 + params: {
787 + keys: ['preferences']
788 + },
789 + id: 1
790 + }
791 + },
792 + 'context/status': {
793 + name: 'context/status',
794 + description: 'Get context status',
795 + defaultRequest: {
796 + jsonrpc: '2.0',
797 + method: 'context/status',
798 + params: {},
799 + id: 1
800 + }
801 + },
802 + 'context/save': {
803 + name: 'context/save',
804 + description: 'Save context for future use',
805 + defaultRequest: {
806 + jsonrpc: '2.0',
807 + method: 'context/save',
808 + params: {
809 + name: 'my_saved_context'
810 + },
811 + id: 1
812 + }
813 + },
814 + 'context/load': {
815 + name: 'context/load',
816 + description: 'Load a saved context',
817 + defaultRequest: {
818 + jsonrpc: '2.0',
819 + method: 'context/load',
820 + params: {
821 + name: 'my_saved_context'
822 + },
823 + id: 1
824 + }
825 + }
826 + }
827 + },
828 + 'system-flow': {
829 + name: 'System',
830 + methods: {
831 + 'system/health': {
832 + name: 'system/health',
833 + description: 'Get system health information',
834 + defaultRequest: {
835 + jsonrpc: '2.0',
836 + method: 'system/health',
837 + params: {},
838 + id: 1
839 + }
840 + },
841 + 'system/version': {
842 + name: 'system/version',
843 + description: 'Get detailed version information',
844 + defaultRequest: {
845 + jsonrpc: '2.0',
846 + method: 'system/version',
847 + params: {},
848 + id: 1
849 + }
850 + },
851 + 'system/metrics': {
852 + name: 'system/metrics',
853 + description: 'Get system performance metrics',
854 + defaultRequest: {
855 + jsonrpc: '2.0',
856 + method: 'system/metrics',
857 + params: {},
858 + id: 1
859 + }
860 + },
861 + 'system/restart': {
862 + name: 'system/restart',
863 + description: 'Request system restart',
864 + defaultRequest: {
865 + jsonrpc: '2.0',
866 + method: 'system/restart',
867 + params: {
868 + force: false
869 + },
870 + id: 1
871 + }
872 + },
873 + 'system/status': {
874 + name: 'system/status',
875 + description: 'Get system status',
876 + defaultRequest: {
877 + jsonrpc: '2.0',
878 + method: 'system/status',
879 + params: {},
880 + id: 1
881 + }
882 + }
883 + }
884 + }
885 + // Additional flows can be added here later
886 + };
887 +
888 + // Initialize UI
889 + function initializeUI() {
890 + // Populate flow items
891 + flowsContainer.innerHTML = '';
892 + Object.keys(flows).forEach(flowId => {
893 + const flow = flows[flowId];
894 + const flowElement = document.createElement('div');
895 + flowElement.className = `flow-item ${flowId === currentFlow ? 'active' : ''}`;
896 + flowElement.dataset.flow = flowId;
897 + flowElement.textContent = flow.name;
898 + flowElement.addEventListener('click', () => selectFlow(flowId));
899 + flowsContainer.appendChild(flowElement);
900 + });
901 +
902 + // Load methods for current flow
903 + loadMethodsForFlow(currentFlow);
904 + }
905 +
906 + // Load methods for a specific flow
907 + function loadMethodsForFlow(flowId) {
908 + methodsContainer.innerHTML = '';
909 + const flow = flows[flowId];
910 +
911 + if (!flow) return;
912 +
913 + Object.keys(flow.methods).forEach(methodId => {
914 + const method = flow.methods[methodId];
915 + const methodElement = document.createElement('div');
916 + methodElement.className = 'method-item';
917 + methodElement.dataset.method = methodId;
918 + methodElement.dataset.flow = flowId;
919 +
920 + const methodName = document.createElement('div');
921 + methodName.textContent = method.name;
922 + methodName.style.fontWeight = 'bold';
923 +
924 + const methodDesc = document.createElement('div');
925 + methodDesc.textContent = method.description;
926 + methodDesc.style.fontSize = '0.9em';
927 + methodDesc.style.color = '#666';
928 +
929 + methodElement.appendChild(methodName);
930 + methodElement.appendChild(methodDesc);
931 +
932 + methodElement.addEventListener('click', () => selectMethod(flowId, methodId));
933 + methodsContainer.appendChild(methodElement);
934 + });
935 +
936 + // Select the first method by default, or restore the previously selected method
937 + const savedMethod = localStorage.getItem(`selected-method-${flowId}`);
938 + if (savedMethod && flows[flowId].methods[savedMethod]) {
939 + selectMethod(flowId, savedMethod);
940 + } else {
941 + const firstMethod = Object.keys(flow.methods)[0];
942 + selectMethod(flowId, firstMethod);
943 + }
944 + }
945 +
946 + // Select a flow
947 + function selectFlow(flowId) {
948 + // Update UI
949 + document.querySelectorAll('.flow-item').forEach(item => {
950 + item.classList.toggle('active', item.dataset.flow === flowId);
951 + });
952 +
953 + // Save current request if a method is selected
954 + if (currentMethod) {
955 + saveCurrentRequest();
956 + }
957 +
958 + // Update state
959 + currentFlow = flowId;
960 + localStorage.setItem('selected-flow', flowId);
961 +
962 + // Load methods for this flow
963 + loadMethodsForFlow(flowId);
964 + }
965 +
966 + // Select a method
967 + function selectMethod(flowId, methodId) {
968 + // Save current request if a method is already selected
969 + if (currentMethod) {
970 + saveCurrentRequest();
971 + }
972 +
973 + // Update UI
974 + document.querySelectorAll('.method-item').forEach(item => {
975 + item.classList.toggle('active', item.dataset.method === methodId);
976 + });
977 +
978 + // Update state
979 + currentFlow = flowId;
980 + currentMethod = methodId;
981 + localStorage.setItem('selected-flow', flowId);
982 + localStorage.setItem(`selected-method-${flowId}`, methodId);
983 +
984 + // Load saved or default request for this method
985 + loadRequestForMethod(flowId, methodId);
986 +
987 + // Load saved response for this method if exists
988 + loadResponseForMethod(flowId, methodId);
989 + }
990 +
991 + // Save the current request in the editor
992 + function saveCurrentRequest() {
993 + if (!currentFlow || !currentMethod) return;
994 +
995 + try {
996 + const requestObj = JSON.parse(jsonEditor.value);
997 + const key = `${currentFlow}:${currentMethod}`;
998 +
999 + if (!methodState.has(key)) {
1000 + methodState.set(key, {});
1001 + }
1002 +
1003 + methodState.get(key).request = requestObj;
1004 + localStorage.setItem(`request:${key}`, jsonEditor.value);
1005 + } catch (error) {
1006 + console.error('Failed to save request:', error);
1007 + }
1008 + }
1009 +
1010 + // Load a request for the selected method
1011 + function loadRequestForMethod(flowId, methodId) {
1012 + const key = `${flowId}:${methodId}`;
1013 + const defaultRequest = flows[flowId].methods[methodId].defaultRequest;
1014 +
1015 + // Try to load from session storage
1016 + let savedRequest = localStorage.getItem(`request:${key}`);
1017 +
1018 + if (savedRequest) {
1019 + jsonEditor.value = savedRequest;
1020 + } else if (methodState.has(key) && methodState.get(key).request) {
1021 + jsonEditor.value = JSON.stringify(methodState.get(key).request, null, 4);
1022 + } else {
1023 + // Use default
1024 + jsonEditor.value = JSON.stringify(defaultRequest, null, 4);
1025 + }
1026 + }
1027 +
1028 + // Load a saved response for the selected method
1029 + function loadResponseForMethod(flowId, methodId) {
1030 + const key = `${flowId}:${methodId}`;
1031 +
1032 + if (methodState.has(key) && methodState.get(key).response) {
1033 + responseViewer.innerHTML = syntaxHighlightJson(methodState.get(key).response);
1034 + if (methodState.get(key).requestSent) {
1035 + requestData.textContent = 'Request: ' + JSON.stringify(methodState.get(key).requestSent, null, 2);
1036 + } else {
1037 + requestData.textContent = '';
1038 + }
1039 +
1040 + // Show communication log if available
1041 + if (methodState.get(key).commLog) {
1042 + commLog.innerHTML = methodState.get(key).commLog;
1043 + } else {
1044 + commLog.innerHTML = '';
1045 + }
1046 + } else {
1047 + responseViewer.innerHTML = '<span class="json-null">No response yet.</span>';
1048 + requestData.textContent = '';
1049 + commLog.innerHTML = '';
1050 + }
1051 + }
1052 +
1053 + // Reset request to default
1054 + resetBtn.addEventListener('click', () => {
1055 + if (!currentFlow || !currentMethod) return;
1056 +
1057 + const defaultRequest = flows[currentFlow].methods[currentMethod].defaultRequest;
1058 + jsonEditor.value = JSON.stringify(defaultRequest, null, 4);
1059 +
1060 + // Update the next request ID
1061 + if (defaultRequest.id) {
1062 + defaultRequest.id = requestId++;
1063 + jsonEditor.value = JSON.stringify(defaultRequest, null, 4);
1064 + }
1065 + });
1066 +
1067 + // Clear communication log
1068 + clearLogBtn.addEventListener('click', () => {
1069 + commLog.innerHTML = '';
1070 +
1071 + // Clear the log in the current method state
1072 + if (currentFlow && currentMethod) {
1073 + const key = `${currentFlow}:${currentMethod}`;
1074 + if (methodState.has(key)) {
1075 + methodState.get(key).commLog = '';
1076 + }
1077 + }
1078 + });
1079 +
1080 + // Connect to WebSocket server
1081 + connectBtn.addEventListener('click', () => {
1082 + const url = serverUrlInput.value.trim();
1083 +
1084 + if (!url) {
1085 + alert('Please enter a WebSocket server URL');
1086 + return;
1087 + }
1088 +
1089 + try {
1090 + socket = new WebSocket(url);
1091 +
1092 + socket.onopen = () => {
1093 + isConnected = true;
1094 + statusSpan.textContent = 'Connected';
1095 + statusSpan.classList.remove('disconnected');
1096 + statusSpan.classList.add('connected');
1097 + connectBtn.disabled = true;
1098 + disconnectBtn.disabled = false;
1099 + sendBtn.disabled = false;
1100 + };
1101 +
1102 + socket.onclose = (event) => {
1103 + isConnected = false;
1104 + statusSpan.textContent = 'Disconnected';
1105 + statusSpan.classList.remove('connected');
1106 + statusSpan.classList.add('disconnected');
1107 + connectBtn.disabled = false;
1108 + disconnectBtn.disabled = true;
1109 + sendBtn.disabled = true;
1110 + };
1111 +
1112 + socket.onerror = (error) => {
1113 + console.error('WebSocket error:', error);
1114 + };
1115 +
1116 + socket.onmessage = (event) => {
1117 + try {
1118 + // Calculate response time
1119 + const responseTimestamp = new Date();
1120 + const responseTimeStr = responseTimestamp.toLocaleTimeString() + '.' +
1121 + String(responseTimestamp.getMilliseconds()).padStart(3, '0');
1122 +
1123 + // Calculate response size
1124 + const responseSize = new Blob([event.data]).size;
1125 +
1126 + const response = JSON.parse(event.data);
1127 +
1128 + // Display the response with syntax highlighting
1129 + responseViewer.innerHTML = syntaxHighlightJson(response);
1130 +
1131 + // Update communication log
1132 + let logEntry = '';
1133 + if (requestTimestamp) {
1134 + const elapsed = responseTimestamp - requestTimestamp;
1135 + logEntry = `[${responseTimeStr}] received response (${responseSize} bytes), latency ${elapsed}ms\n`;
1136 + commLog.innerHTML += logEntry;
1137 +
1138 + // Auto-scroll the log to the bottom
1139 + commLog.scrollTop = commLog.scrollHeight;
1140 + }
1141 +
1142 + // Save the response and log for the current method
1143 + if (currentFlow && currentMethod) {
1144 + const key = `${currentFlow}:${currentMethod}`;
1145 +
1146 + if (!methodState.has(key)) {
1147 + methodState.set(key, {});
1148 + }
1149 +
1150 + methodState.get(key).response = response;
1151 + methodState.get(key).responseTimestamp = responseTimestamp;
1152 + methodState.get(key).commLog = commLog.innerHTML;
1153 + }
1154 + } catch (error) {
1155 + // If parse fails, display the raw data
1156 + responseViewer.innerHTML = `<span class="json-string">${escapeHtml(event.data)}</span>`;
1157 + console.error('Error parsing JSON response:', error);
1158 + }
1159 + };
1160 +
1161 + } catch (error) {
1162 + alert(`Failed to connect: ${error.message}`);
1163 + console.error('Connection error:', error);
1164 + }
1165 + });
1166 +
1167 + // Disconnect from WebSocket server
1168 + disconnectBtn.addEventListener('click', () => {
1169 + if (socket) {
1170 + socket.close(1000, 'User initiated disconnect');
1171 + socket = null;
1172 + }
1173 + });
1174 +
1175 + // Send request
1176 + sendBtn.addEventListener('click', () => {
1177 + if (!socket || socket.readyState !== WebSocket.OPEN) {
1178 + alert('Not connected to the WebSocket server');
1179 + return;
1180 + }
1181 +
1182 + try {
1183 + const requestJson = jsonEditor.value.trim();
1184 + const requestObj = JSON.parse(requestJson);
1185 +
1186 + // Add an ID if not present and it's not a notification
1187 + if (requestObj.method && requestObj.method !== 'notification' && !requestObj.id) {
1188 + requestObj.id = requestId++;
1189 + jsonEditor.value = JSON.stringify(requestObj, null, 4);
1190 + }
1191 +
1192 + // Ensure jsonrpc version is set
1193 + if (!requestObj.jsonrpc) {
1194 + requestObj.jsonrpc = '2.0';
1195 + jsonEditor.value = JSON.stringify(requestObj, null, 4);
1196 + }
1197 +
1198 + const request = JSON.stringify(requestObj);
1199 + const requestSize = new Blob([request]).size;
1200 +
1201 + // Record request timestamp
1202 + requestTimestamp = new Date();
1203 + const requestTimeStr = requestTimestamp.toLocaleTimeString() + '.' +
1204 + String(requestTimestamp.getMilliseconds()).padStart(3, '0');
1205 +
1206 + // Add to the communication log
1207 + const logEntry = `[${requestTimeStr}] sending request (${requestSize} bytes)\n`;
1208 + commLog.innerHTML += logEntry;
1209 +
1210 + // Auto-scroll the log to the bottom
1211 + commLog.scrollTop = commLog.scrollHeight;
1212 +
1213 + // Send the request
1214 + socket.send(request);
1215 +
1216 + // Update request display
1217 + requestData.textContent = 'Request: ' + request;
1218 +
1219 + // Save the sent request and timestamp
1220 + if (currentFlow && currentMethod) {
1221 + const key = `${currentFlow}:${currentMethod}`;
1222 +
1223 + if (!methodState.has(key)) {
1224 + methodState.set(key, {});
1225 + }
1226 +
1227 + methodState.get(key).requestSent = requestObj;
1228 + methodState.get(key).requestTimestamp = requestTimestamp;
1229 + methodState.get(key).commLog = commLog.innerHTML;
1230 + }
1231 +
1232 + } catch (error) {
1233 + alert(`Error sending request: ${error.message}`);
1234 + console.error('Send error:', error);
1235 + }
1236 + });
1237 +
1238 + // Load saved state from localStorage
1239 + function loadSavedState() {
1240 + const savedFlow = localStorage.getItem('selected-flow');
1241 + if (savedFlow && flows[savedFlow]) {
1242 + currentFlow = savedFlow;
1243 + }
1244 +
1245 + // For each flow, load saved requests
1246 + Object.keys(flows).forEach(flowId => {
1247 + const flow = flows[flowId];
1248 + Object.keys(flow.methods).forEach(methodId => {
1249 + const key = `${flowId}:${methodId}`;
1250 + const savedRequest = localStorage.getItem(`request:${key}`);
1251 +
1252 + if (savedRequest) {
1253 + try {
1254 + const requestObj = JSON.parse(savedRequest);
1255 + if (!methodState.has(key)) {
1256 + methodState.set(key, {});
1257 + }
1258 + methodState.get(key).request = requestObj;
1259 + } catch (error) {
1260 + console.error(`Failed to parse saved request for ${key}:`, error);
1261 + }
1262 + }
1263 + });
1264 + });
1265 + }
1266 +
1267 + // Initialize the UI
1268 + loadSavedState();
1269 + initializeUI();
1270 + </script>
1271 +</body>
1272 +</html>
\ No newline at end of file
src/web/mcp/mcp.c new
+436
@@ -0,0 +1,436 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "mcp.h"
4 +#include "mcp-initialize.h"
5 +#include "mcp-tools.h"
6 +#include "mcp-resources.h"
7 +#include "mcp-prompts.h"
8 +#include "mcp-notifications.h"
9 +#include "mcp-context.h"
10 +#include "mcp-system.h"
11 +#include "adapters/mcp-websocket.h"
12 +
13 +// Define the enum to string mapping for protocol versions
14 +ENUM_STR_MAP_DEFINE(MCP_PROTOCOL_VERSION) = {
15 + { .id = MCP_PROTOCOL_VERSION_2024_11_05, .name = "2024-11-05" },
16 + { .id = MCP_PROTOCOL_VERSION_2025_03_26, .name = "2025-03-26" },
17 + { .id = MCP_PROTOCOL_VERSION_UNKNOWN, .name = "unknown" },
18 +
19 + // terminator
20 + { .name = NULL, .id = 0 }
21 +};
22 +ENUM_STR_DEFINE_FUNCTIONS(MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSION_UNKNOWN, "unknown");
23 +
24 +// Define the enum to string mapping for return codes
25 +ENUM_STR_MAP_DEFINE(MCP_RETURN_CODE) = {
26 + { .id = MCP_RC_OK, .name = "OK" },
27 + { .id = MCP_RC_ERROR, .name = "ERROR" },
28 + { .id = MCP_RC_INVALID_PARAMS, .name = "INVALID_PARAMS" },
29 + { .id = MCP_RC_NOT_FOUND, .name = "NOT_FOUND" },
30 + { .id = MCP_RC_INTERNAL_ERROR, .name = "INTERNAL_ERROR" },
31 + { .id = MCP_RC_NOT_IMPLEMENTED, .name = "NOT_IMPLEMENTED" },
32 +
33 + // terminator
34 + { .name = NULL, .id = 0 }
35 +};
36 +ENUM_STR_DEFINE_FUNCTIONS(MCP_RETURN_CODE, MCP_RC_ERROR, "ERROR");
37 +
38 +// Decode a URI component using mcpc's pre-allocated buffer
39 +// Returns a pointer to the decoded string which is valid until the next call
40 +const char *mcp_uri_decode(MCP_CLIENT *mcpc, const char *src) {
41 + if(!mcpc || !src || !*src)
42 + return src;
43 +
44 + // Prepare the buffer
45 + buffer_flush(mcpc->uri);
46 + buffer_need_bytes(mcpc->uri, strlen(src) + 1);
47 +
48 + // Perform URL decoding
49 + char *d = url_decode_r(mcpc->uri->buffer, src, mcpc->uri->size);
50 + if (!d || !*d)
51 + return src;
52 +
53 + // Ensure the buffer's length is updated
54 + mcpc->uri->len = strlen(d);
55 +
56 + return buffer_tostring(mcpc->uri);
57 +}
58 +
59 +// Create a response context for a transport session
60 +MCP_CLIENT *mcp_create_client(MCP_TRANSPORT transport, void *transport_ctx) {
61 + MCP_CLIENT *ctx = callocz(1, sizeof(MCP_CLIENT));
62 +
63 + ctx->transport = transport;
64 + ctx->protocol_version = MCP_PROTOCOL_VERSION_UNKNOWN; // Will be set during initialization
65 +
66 + // Set capabilities based on transport type
67 + switch (transport) {
68 + case MCP_TRANSPORT_WEBSOCKET:
69 + ctx->websocket = (struct websocket_server_client *)transport_ctx;
70 + ctx->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
71 + MCP_CAPABILITY_SUBSCRIPTIONS |
72 + MCP_CAPABILITY_NOTIFICATIONS;
73 + break;
74 +
75 + case MCP_TRANSPORT_HTTP:
76 + ctx->http = (struct web_client *)transport_ctx;
77 + ctx->capabilities = MCP_CAPABILITY_NONE; // HTTP has no special capabilities
78 + break;
79 +
80 + default:
81 + ctx->generic = transport_ctx;
82 + ctx->capabilities = MCP_CAPABILITY_NONE;
83 + break;
84 + }
85 +
86 + // Default client info (will be updated later from actual client)
87 + ctx->client_name = string_strdupz("unknown");
88 + ctx->client_version = string_strdupz("0.0.0");
89 +
90 + // Initialize response buffers
91 + ctx->result = buffer_create(4096, NULL);
92 + ctx->error = buffer_create(1024, NULL);
93 +
94 + // Initialize utility buffers
95 + ctx->uri = buffer_create(1024, NULL);
96 +
97 + return ctx;
98 +}
99 +
100 +// Free a response context
101 +void mcp_free_client(MCP_CLIENT *mcpc) {
102 + if (mcpc) {
103 + string_freez(mcpc->client_name);
104 + string_freez(mcpc->client_version);
105 +
106 + // Free response buffers
107 + buffer_free(mcpc->result);
108 + buffer_free(mcpc->error);
109 +
110 + // Free utility buffers
111 + buffer_free(mcpc->uri);
112 +
113 + freez(mcpc);
114 + }
115 +}
116 +
117 +// Map internal MCP_RETURN_CODE to JSON-RPC error code
118 +static int mcp_map_return_code_to_jsonrpc_error(MCP_RETURN_CODE rc) {
119 + switch (rc) {
120 + case MCP_RC_OK:
121 + return 0; // Not an error
122 + case MCP_RC_INVALID_PARAMS:
123 + return -32602; // JSON-RPC Invalid params
124 + case MCP_RC_NOT_FOUND:
125 + return -32601; // JSON-RPC Method not found
126 + case MCP_RC_INTERNAL_ERROR:
127 + return -32603; // JSON-RPC Internal error
128 + case MCP_RC_NOT_IMPLEMENTED:
129 + return -32601; // Use method not found for not implemented
130 + case MCP_RC_ERROR:
131 + default:
132 + return -32000; // JSON-RPC Server error
133 + }
134 +}
135 +
136 +void mcp_init_success_result(MCP_CLIENT *mcpc, uint64_t id) {
137 + buffer_flush(mcpc->result);;
138 + buffer_json_initialize(mcpc->result, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
139 + buffer_json_member_add_string(mcpc->result, "jsonrpc", "2.0");
140 +
141 + if(id)
142 + buffer_json_member_add_uint64(mcpc->result, "id", id);
143 +
144 + buffer_flush(mcpc->error);
145 +}
146 +
147 +void mcp_jsonrpc_error(BUFFER *result, const char *error, uint64_t id, int jsonrpc_code) {
148 + buffer_flush(result);
149 + buffer_json_initialize(result, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
150 + buffer_json_member_add_string(result, "jsonrpc", "2.0");
151 +
152 + if (id)
153 + buffer_json_member_add_uint64(result, "id", id);
154 +
155 + buffer_json_member_add_int64(result, "error", jsonrpc_code);
156 +
157 + if(error && *error)
158 + buffer_json_member_add_string(result, "message", error);
159 +
160 + buffer_json_finalize(result);
161 +}
162 +
163 +MCP_RETURN_CODE mcp_error_result(MCP_CLIENT *mcpc, uint64_t id, MCP_RETURN_CODE rc) {
164 + mcp_jsonrpc_error(mcpc->result,
165 + buffer_strlen(mcpc->error) ? buffer_tostring(mcpc->error) : MCP_RETURN_CODE_2str(rc),
166 + id, mcp_map_return_code_to_jsonrpc_error(rc));
167 + return rc;
168 +}
169 +
170 +// Send the content of a buffer using the appropriate transport
171 +int mcp_send_response_buffer(MCP_CLIENT *mcpc) {
172 + if (!mcpc || !mcpc->result || !buffer_strlen(mcpc->result)) return -1;
173 +
174 + switch (mcpc->transport) {
175 + case MCP_TRANSPORT_WEBSOCKET:
176 + return mcp_websocket_send_buffer(mcpc->websocket, mcpc->result);
177 +
178 + case MCP_TRANSPORT_HTTP:
179 + netdata_log_error("MCP: HTTP adapter not implemented yet");
180 + return -1;
181 +
182 + default:
183 + netdata_log_error("MCP: Unknown transport type %u", mcpc->transport);
184 + return -1;
185 + }
186 +}
187 +
188 +// Parse and extract client info from initialize request params
189 +static void mcp_extract_client_info(MCP_CLIENT *ctx, struct json_object *params) {
190 + if (!ctx || !params) return;
191 +
192 + struct json_object *client_info_obj = NULL;
193 + struct json_object *client_name_obj = NULL;
194 + struct json_object *client_version_obj = NULL;
195 +
196 + if (json_object_object_get_ex(params, "clientInfo", &client_info_obj)) {
197 + if (json_object_object_get_ex(client_info_obj, "name", &client_name_obj)) {
198 + string_freez(ctx->client_name);
199 + ctx->client_name = string_strdupz(json_object_get_string(client_name_obj));
200 + }
201 + if (json_object_object_get_ex(client_info_obj, "version", &client_version_obj)) {
202 + string_freez(ctx->client_version);
203 + ctx->client_version = string_strdupz(json_object_get_string(client_version_obj));
204 + }
205 + }
206 +}
207 +
208 +// Handle a JSON-RPC method call - the result is always filled with a jsonrpc response
209 +static MCP_RETURN_CODE mcp_single_request(MCP_CLIENT *mcpc, struct json_object *request) {
210 + if (!mcpc || !request) {
211 + return MCP_RC_ERROR;
212 + }
213 +
214 + // Flush buffers before processing the request
215 + buffer_flush(mcpc->result);
216 + buffer_flush(mcpc->error);
217 +
218 + // Extract JSON-RPC fields
219 + struct json_object *method_obj = NULL;
220 + struct json_object *params_obj = NULL;
221 + struct json_object *id_obj = NULL;
222 + struct json_object *jsonrpc_obj = NULL;
223 +
224 + // Validate jsonrpc version
225 + if (!json_object_object_get_ex(request, "jsonrpc", &jsonrpc_obj) ||
226 + strcmp(json_object_get_string(jsonrpc_obj), "2.0") != 0) {
227 + buffer_strcat(mcpc->error, "Invalid or missing jsonrpc version");
228 + mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
229 + return MCP_RC_INVALID_PARAMS;
230 + }
231 +
232 + // Extract method
233 + if (!json_object_object_get_ex(request, "method", &method_obj)) {
234 + buffer_strcat(mcpc->error, "Missing method field");
235 + mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
236 + return MCP_RC_INVALID_PARAMS;
237 + }
238 +
239 + const char *method = json_object_get_string(method_obj);
240 +
241 + // Extract params (optional)
242 + if (json_object_object_get_ex(request, "params", &params_obj)) {
243 + if (json_object_get_type(params_obj) != json_type_object) {
244 + buffer_strcat(mcpc->error, "params must be an object");
245 + mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
246 + return MCP_RC_INVALID_PARAMS;
247 + }
248 + } else {
249 + // Create an empty params object if none provided
250 + params_obj = json_object_new_object();
251 + }
252 +
253 + // Extract ID (optional, for notifications)
254 + uint64_t id = 0;
255 + bool has_id = json_object_object_get_ex(request, "id", &id_obj);
256 +
257 + if (has_id) {
258 + if (json_object_get_type(id_obj) == json_type_int) {
259 + id = json_object_get_int64(id_obj);
260 + }
261 + else if (json_object_get_type(id_obj) == json_type_string) {
262 + const char *id_str = json_object_get_string(id_obj);
263 + char *endptr;
264 + id = strtoull(id_str, &endptr, 10);
265 + if (*endptr != '\0') {
266 + // If the string is not a number, use a hash of the string as the ID
267 + id = 0;
268 + while (*id_str) {
269 + id = id * 31 + (*id_str++);
270 + }
271 + }
272 + }
273 + }
274 +
275 + netdata_log_debug(D_WEB_CLIENT, "MCP: Handling method call: %s (id: %"PRIu64")", method, id);
276 +
277 + // Handle method calls based on namespace
278 + MCP_RETURN_CODE rc;
279 +
280 + if(!method || !*method) {
281 + buffer_strcat(mcpc->error, "Empty method name");
282 + rc = MCP_RC_INVALID_PARAMS;
283 + }
284 + else if (strncmp(method, "tools/", 6) == 0) {
285 + // Tools namespace
286 + rc = mcp_tools_route(mcpc, method + 6, params_obj, id);
287 + }
288 + else if (strncmp(method, "resources/", 10) == 0) {
289 + // Resources namespace
290 + rc = mcp_resources_route(mcpc, method + 10, params_obj, id);
291 + }
292 + else if (strncmp(method, "notifications/", 14) == 0) {
293 + // Notifications namespace
294 + rc = mcp_notifications_route(mcpc, method + 14, params_obj, id);
295 + }
296 + else if (strncmp(method, "prompts/", 8) == 0) {
297 + // Prompts namespace
298 + rc = mcp_prompts_route(mcpc, method + 8, params_obj, id);
299 + }
300 + else if (strncmp(method, "context/", 8) == 0) {
301 + // Context namespace
302 + rc = mcp_context_route(mcpc, method + 8, params_obj, id);
303 + }
304 + else if (strncmp(method, "system/", 7) == 0) {
305 + // System namespace
306 + rc = mcp_system_route(mcpc, method + 7, params_obj, id);
307 + }
308 + else if (strcmp(method, "initialize") == 0) {
309 + // Extract client info from initialize request
310 + mcp_extract_client_info(mcpc, params_obj);
311 + netdata_log_debug(D_WEB_CLIENT, "MCP initialize request from client %s v%s",
312 + string2str(mcpc->client_name), string2str(mcpc->client_version));
313 +
314 + // Handle initialize method
315 + rc = mcp_method_initialize(mcpc, params_obj, id);
316 + }
317 + else {
318 + buffer_sprintf(mcpc->error, "Method '%s' not found", method);
319 + rc = MCP_RC_NOT_FOUND;
320 + }
321 +
322 + // If this is a notification (no ID), don't generate a response
323 + if (!has_id) {
324 + return rc;
325 + }
326 +
327 + // For requests with IDs, ensure we have a valid response
328 + if (rc != MCP_RC_OK && !buffer_strlen(mcpc->result)) {
329 + mcp_error_result(mcpc, id, rc);
330 + }
331 +
332 + if (!buffer_strlen(mcpc->result)) {
333 + buffer_strcat(mcpc->error, "method generated empty result");
334 + mcp_error_result(mcpc, id, MCP_RC_INTERNAL_ERROR);
335 + }
336 +
337 + return rc;
338 +}
339 +
340 +// Main MCP entry point - handle a JSON-RPC request (can be single or batch)
341 +MCP_RETURN_CODE mcp_handle_request(MCP_CLIENT *mcpc, struct json_object *request) {
342 + if (!mcpc || !request) return MCP_RC_INTERNAL_ERROR;
343 +
344 + // Clear previous response buffers
345 + buffer_flush(mcpc->result);
346 + buffer_flush(mcpc->error);
347 +
348 + // Check if this is a batch request (JSON array)
349 + if (json_object_get_type(request) == json_type_array) {
350 + int array_len = json_object_array_length(request);
351 +
352 + // Empty batch should return nothing according to JSON-RPC 2.0 spec
353 + if (array_len == 0) {
354 + return MCP_RC_OK;
355 + }
356 +
357 + // Create a temporary buffer for building the batch response
358 + BUFFER *batch_buffer = buffer_create(4096, NULL);
359 + buffer_flush(batch_buffer);
360 +
361 + // Start the JSON array for batch response
362 + buffer_strcat(batch_buffer, "[");
363 +
364 + // Track if we've added any responses (for comma handling)
365 + bool has_responses = false;
366 +
367 + // Process each request in the batch
368 + for (int i = 0; i < array_len; i++) {
369 + struct json_object *req_item = json_object_array_get_idx(request, i);
370 +
371 + // Process the individual request
372 + buffer_flush(mcpc->result);
373 + buffer_flush(mcpc->error);
374 +
375 + // Extract ID to determine if it's a request or notification
376 + struct json_object *id_obj = NULL;
377 + bool has_id = json_object_object_get_ex(req_item, "id", &id_obj);
378 +
379 + // Call the single request handler
380 + mcp_single_request(mcpc, req_item);
381 +
382 + // For notifications (no id), don't add to response
383 + if (!has_id || buffer_strlen(mcpc->result) == 0) {
384 + continue;
385 + }
386 +
387 + // Add comma if this isn't the first response
388 + if (has_responses) {
389 + buffer_strcat(batch_buffer, ", ");
390 + }
391 +
392 + // Add the response to the batch
393 + buffer_strcat(batch_buffer, buffer_tostring(mcpc->result));
394 + has_responses = true;
395 + }
396 +
397 + // If no responses were added (all notifications), don't send anything per JSON-RPC spec
398 + if (!has_responses) {
399 + buffer_free(batch_buffer);
400 + return MCP_RC_OK;
401 + }
402 +
403 + // Close the JSON array
404 + buffer_strcat(batch_buffer, "]");
405 +
406 + // Copy batch response to client's result buffer
407 + buffer_flush(mcpc->result);
408 + buffer_strcat(mcpc->result, buffer_tostring(batch_buffer));
409 + buffer_free(batch_buffer);
410 +
411 + // Send the batch response
412 + mcp_send_response_buffer(mcpc);
413 +
414 + return MCP_RC_OK;
415 + }
416 + else {
417 + // Handle single request
418 + MCP_RETURN_CODE rc = mcp_single_request(mcpc, request);
419 +
420 + // Extract ID to determine if it's a request or notification
421 + struct json_object *id_obj = NULL;
422 + bool has_id = json_object_object_get_ex(request, "id", &id_obj);
423 +
424 + // Only send responses for requests with IDs, not for notifications
425 + if (has_id && buffer_strlen(mcpc->result) > 0) {
426 + mcp_send_response_buffer(mcpc);
427 + }
428 +
429 + return rc;
430 + }
431 +}
432 +
433 +// Initialize the MCP subsystem
434 +void mcp_initialize_subsystem(void) {
435 + netdata_log_info("MCP subsystem initialized");
436 +}
src/web/mcp/mcp.h new
+134
@@ -0,0 +1,134 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_H
4 +#define NETDATA_MCP_H
5 +
6 +#include "libnetdata/libnetdata.h"
7 +#include <json-c/json.h>
8 +
9 +// MCP protocol versions
10 +typedef enum {
11 + MCP_PROTOCOL_VERSION_UNKNOWN = 0,
12 + MCP_PROTOCOL_VERSION_2024_11_05 = 20241105, // Using numeric date format for natural ordering
13 + MCP_PROTOCOL_VERSION_2025_03_26 = 20250326,
14 + // Add future versions here
15 +
16 + // Always keep this pointing to the latest version
17 + MCP_PROTOCOL_VERSION_LATEST = MCP_PROTOCOL_VERSION_2025_03_26
18 +} MCP_PROTOCOL_VERSION;
19 +ENUM_STR_DEFINE_FUNCTIONS_EXTERN(MCP_PROTOCOL_VERSION);
20 +
21 +// JSON-RPC error codes (standard)
22 +#define MCP_ERROR_PARSE_ERROR -32700
23 +#define MCP_ERROR_INVALID_REQUEST -32600
24 +#define MCP_ERROR_METHOD_NOT_FOUND -32601
25 +#define MCP_ERROR_INVALID_PARAMS -32602
26 +#define MCP_ERROR_INTERNAL_ERROR -32603
27 +// Server error codes (implementation-defined)
28 +#define MCP_ERROR_SERVER_ERROR_MIN -32099
29 +#define MCP_ERROR_SERVER_ERROR_MAX -32000
30 +
31 +// Content types (for messages and tool responses)
32 +typedef enum {
33 + MCP_CONTENT_TYPE_TEXT = 0,
34 + MCP_CONTENT_TYPE_IMAGE = 1,
35 + MCP_CONTENT_TYPE_AUDIO = 2, // New in 2025-03-26
36 +} MCP_CONTENT_TYPE;
37 +
38 +// Forward declarations for transport-specific types
39 +struct websocket_server_client;
40 +struct web_client;
41 +
42 +// Transport types for MCP
43 +typedef enum {
44 + MCP_TRANSPORT_UNKNOWN = 0,
45 + MCP_TRANSPORT_WEBSOCKET,
46 + MCP_TRANSPORT_HTTP,
47 + // Add more as needed
48 +} MCP_TRANSPORT;
49 +
50 +// Transport capabilities
51 +typedef enum {
52 + MCP_CAPABILITY_NONE = 0,
53 + MCP_CAPABILITY_ASYNC_COMMUNICATION = (1 << 0), // Can send messages at any time
54 + MCP_CAPABILITY_SUBSCRIPTIONS = (1 << 1), // Supports subscriptions
55 + MCP_CAPABILITY_NOTIFICATIONS = (1 << 2), // Supports notifications
56 + // Add more as needed
57 +} MCP_CAPABILITY;
58 +
59 +// Return codes for MCP functions
60 +typedef enum {
61 + MCP_RC_OK = 0, // Success, result buffer contains valid response
62 + MCP_RC_ERROR = 1, // Generic error, error buffer contains message
63 + MCP_RC_INVALID_PARAMS = 2, // Invalid parameters in request
64 + MCP_RC_NOT_FOUND = 3, // Resource or method not found
65 + MCP_RC_INTERNAL_ERROR = 4, // Internal server error
66 + MCP_RC_NOT_IMPLEMENTED = 5 // Method not implemented
67 + // Can add more specific errors as needed
68 +} MCP_RETURN_CODE;
69 +ENUM_STR_DEFINE_FUNCTIONS_EXTERN(MCP_RETURN_CODE);
70 +
71 +// Response handling context
72 +typedef struct mcp_client {
73 + // Transport type and capabilities
74 + MCP_TRANSPORT transport;
75 + MCP_CAPABILITY capabilities;
76 +
77 + // Protocol version (detected during initialization)
78 + MCP_PROTOCOL_VERSION protocol_version;
79 +
80 + // Transport-specific context
81 + union {
82 + struct websocket_server_client *websocket; // WebSocket client
83 + struct web_client *http; // HTTP client
84 + void *generic; // Generic context
85 + };
86 +
87 + // Client information
88 + STRING *client_name; // Client name (for logging, interned)
89 + STRING *client_version; // Client version (for logging, interned)
90 +
91 + // Response buffers
92 + BUFFER *result; // Pre-allocated buffer for success responses
93 + BUFFER *error; // Pre-allocated buffer for error messages
94 +
95 + // Utility buffers
96 + BUFFER *uri; // Pre-allocated buffer for URI decoding
97 +} MCP_CLIENT;
98 +
99 +// Helper function to convert string version to numeric version
100 +MCP_PROTOCOL_VERSION mcp_protocol_version_from_string(const char *version_str);
101 +
102 +// Helper function to convert numeric version to string version
103 +const char *mcp_protocol_version_to_string(MCP_PROTOCOL_VERSION version);
104 +
105 +// Create a response context for a transport session
106 +MCP_CLIENT *mcp_create_client(MCP_TRANSPORT transport, void *transport_ctx);
107 +
108 +// Free a response context
109 +void mcp_free_client(MCP_CLIENT *mcpc);
110 +
111 +// Helper functions for creating and sending JSON-RPC responses
112 +
113 +// Functions to initialize and build MCP responses
114 +void mcp_init_success_result(MCP_CLIENT *mcpc, uint64_t id);
115 +MCP_RETURN_CODE mcp_error_result(MCP_CLIENT *mcpc, uint64_t id, MCP_RETURN_CODE rc);
116 +void mcp_jsonrpc_error(BUFFER *result, const char *error, uint64_t id, int jsonrpc_code);
117 +
118 +// Send prepared buffer content as response
119 +int mcp_send_response_buffer(MCP_CLIENT *mcpc);
120 +
121 +// Check if a capability is supported by the transport
122 +static inline bool mcp_has_capability(MCP_CLIENT *mcpc, MCP_CAPABILITY capability) {
123 + return mcpc && (mcpc->capabilities & capability);
124 +}
125 +
126 +// Initialize the MCP subsystem
127 +void mcp_initialize_subsystem(void);
128 +
129 +// Main MCP entry point - handle a JSON-RPC request (single or batch)
130 +MCP_RETURN_CODE mcp_handle_request(MCP_CLIENT *mcpc, struct json_object *request);
131 +
132 +const char *mcp_uri_decode(MCP_CLIENT *mcpc, const char *src);
133 +
134 +#endif // NETDATA_MCP_H
src/web/server/static/static-threaded.c
+12 -2
@@ -166,6 +166,13 @@ static void web_server_del_callback(POLLINFO *pi) {
166 worker_is_idle();
167 }
168
169 +static __thread POLLINFO *current_thread_pollinfo = NULL;
170 +
171 +void web_server_remove_current_socket_from_poll(void) {
172 + if(!current_thread_pollinfo) return;
173 + poll_process_remove_from_poll(current_thread_pollinfo);
174 +}
175 +
176 static int web_server_rcv_callback(POLLINFO *pi, nd_poll_event_t *events) {
177 int ret = -1;
178 worker_is_busy(WORKER_JOB_RCV_DATA);
@@ -184,7 +191,9 @@ static int web_server_rcv_callback(POLLINFO *pi, nd_poll_event_t *events) {
191 netdata_log_debug(D_WEB_CLIENT, "%llu: processing received data on fd %d.", w->id, fd);
192 worker_is_idle();
193 worker_is_busy(WORKER_JOB_PROCESS);
194 + current_thread_pollinfo = pi;
195 web_client_process_request_from_web_server(w);
196 + current_thread_pollinfo = NULL;
197
198 if (unlikely(w->mode == HTTP_REQUEST_MODE_STREAM)) {
199 ssize_t rc = web_client_send(w);
@@ -226,7 +235,9 @@ static int web_server_snd_callback(POLLINFO *pi, nd_poll_event_t *events) {
235
236 netdata_log_debug(D_WEB_CLIENT, "%llu: sending data on fd %d.", w->id, fd);
237
238 + current_thread_pollinfo = pi;
239 ssize_t ret = web_client_send(w);
240 + current_thread_pollinfo = NULL;
241
242 if(unlikely(ret < 0)) {
243 retval = -1;
@@ -298,8 +309,7 @@ void *socket_listen_main_static_threaded_worker(void *ptr) {
309 , NULL
310 , web_client_first_request_timeout
311 , web_client_timeout
301 - ,
302 - nd_profile.update_every * 1000 // timer_milliseconds
312 + , nd_profile.update_every * 1000 // timer_milliseconds
313 , ptr // timer_data
314 , worker_private->max_sockets
315 );
src/web/server/static/static-threaded.h
+1
@@ -6,5 +6,6 @@
6 #include "web/server/web_server.h"
7
8 void *socket_listen_main_static_threaded(void *ptr);
9 +void web_server_remove_current_socket_from_poll(void);
10
11 #endif //NETDATA_WEB_SERVER_STATIC_THREADED_H
src/web/server/web_client.c
+43 -4
@@ -1,6 +1,7 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "web_client.h"
4 +#include "web/websocket/websocket.h"
5
6 // this is an async I/O implementation of the web server request parser
7 // it is used by all netdata web servers
@@ -163,6 +164,15 @@ static void web_client_reset_allocations(struct web_client *w, bool free_all) {
164
165 freez(w->auth_bearer_token);
166 w->auth_bearer_token = NULL;
167 +
168 + // Free WebSocket resources
169 + freez(w->websocket.key);
170 + w->websocket.key = NULL;
171 +
172 + w->websocket.ext_flags = WS_EXTENSION_NONE;
173 + w->websocket.protocol = WS_PROTOCOL_DEFAULT;
174 + w->websocket.client_max_window_bits = 0;
175 + w->websocket.server_max_window_bits = 0;
176
177 // if we had enabled compression, release it
178 if(w->response.zinitialized) {
@@ -909,7 +919,11 @@ void web_client_build_http_header(struct web_client *w) {
919 }
920
921 static inline void web_client_send_http_header(struct web_client *w) {
912 - web_client_build_http_header(w);
922 + // For WebSocket handshake, the header is already fully prepared in websocket_handle_handshake
923 + // For standard HTTP responses, we need to build the header
924 + if (w->response.code != HTTP_RESP_WEBSOCKET_HANDSHAKE) {
925 + web_client_build_http_header(w);
926 + }
927
928 // sent the HTTP header
929 netdata_log_debug(D_WEB_DATA, "%llu: Sending response HTTP header of size %zu: '%s'"
@@ -1216,8 +1230,6 @@ static inline int web_client_process_url(RRDHOST *host, struct web_client *w, ch
1230 return HTTP_RESP_NOT_FOUND;
1231 }
1232
1219 - debug_flags |= D_RRD_STATS;
1220 -
1233 if(rrdset_flag_check(st, RRDSET_FLAG_DEBUG))
1234 rrdset_flag_clear(st, RRDSET_FLAG_DEBUG);
1235 else
@@ -1303,6 +1315,14 @@ void web_client_process_request_from_web_server(struct web_client *w) {
1315 w->forwarded_for ? w->forwarded_for : w->client_ip);
1316 }
1317
1318 + // Check if this is a WebSocket upgrade request
1319 + // The full WebSocket handshake detection will happen in the header parsing,
1320 + // but we need to set the initial mode to GET for processing to continue
1321 + if (w->mode == HTTP_REQUEST_MODE_GET && web_client_has_websocket_handshake(w) && web_client_is_websocket(w)) {
1322 + w->mode = HTTP_REQUEST_MODE_WEBSOCKET;
1323 + netdata_log_debug(D_WEB_CLIENT, "%llu: Detected WebSocket handshake request", w->id);
1324 + }
1325 +
1326 switch(w->mode) {
1327 case HTTP_REQUEST_MODE_STREAM:
1328 if(unlikely(!http_can_access_stream(w))) {
@@ -1313,6 +1333,21 @@ void web_client_process_request_from_web_server(struct web_client *w) {
1333 w->response.code = stream_receiver_accept_connection(
1334 w, (char *)buffer_tostring(w->url_query_string_decoded), NULL);
1335 return;
1336 +
1337 + case HTTP_REQUEST_MODE_WEBSOCKET:
1338 + if(unlikely(!http_can_access_dashboard(w))) {
1339 + web_client_permission_denied_acl(w);
1340 + return;
1341 + }
1342 +
1343 + // Handle WebSocket handshake - this will take over the socket
1344 + // similar to how stream_receiver_accept_connection works
1345 + w->response.code = websocket_handle_handshake(w);
1346 +
1347 + // After this point the socket has been taken over
1348 + // No need to send a response as the WebSocket handler
1349 + // has already sent the handshake response
1350 + return;
1351
1352 case HTTP_REQUEST_MODE_OPTIONS:
1353 if(unlikely(
@@ -1398,7 +1433,7 @@ void web_client_process_request_from_web_server(struct web_client *w) {
1433 // wait for more data
1434 // set to normal to prevent web_server_rcv_callback
1435 // from going into stream mode
1401 - if (w->mode == HTTP_REQUEST_MODE_STREAM)
1436 + if (w->mode == HTTP_REQUEST_MODE_STREAM || w->mode == HTTP_REQUEST_MODE_WEBSOCKET)
1437 w->mode = HTTP_REQUEST_MODE_GET;
1438 return;
1439 }
@@ -1459,6 +1494,10 @@ void web_client_process_request_from_web_server(struct web_client *w) {
1494 netdata_log_debug(D_WEB_CLIENT, "%llu: STREAM done.", w->id);
1495 break;
1496
1497 + case HTTP_REQUEST_MODE_WEBSOCKET:
1498 + netdata_log_debug(D_WEB_CLIENT, "%llu: Done preparing the WEBSOCKET response..", w->id);
1499 + break;
1500 +
1501 case HTTP_REQUEST_MODE_OPTIONS:
1502 netdata_log_debug(D_WEB_CLIENT,
1503 "%llu: Done preparing the OPTIONS response. Sending data (%zu bytes) to client.",
src/web/server/web_client.h
+22
@@ -4,6 +4,7 @@
4 #define NETDATA_WEB_CLIENT_H 1
5
6 #include "libnetdata/libnetdata.h"
7 +#include "../websocket/websocket.h"
8
9 struct web_client;
10
@@ -66,6 +67,10 @@ typedef enum __attribute__((packed)) {
67
68 // transient settings
69 WEB_CLIENT_FLAG_PROGRESS_TRACKING = (1 << 25), // flag to avoid redoing progress work
70 +
71 + // websocket flags
72 + WEB_CLIENT_FLAG_WEBSOCKET_CLIENT = (1 << 26), // this is a websocket client
73 + WEB_CLIENT_FLAG_WEBSOCKET_HANDSHAKE = (1 << 27), // websocket handshake detected
74 } WEB_CLIENT_FLAGS;
75
76 #define WEB_CLIENT_FLAG_PATH_WITH_VERSION (WEB_CLIENT_FLAG_PATH_IS_V0|WEB_CLIENT_FLAG_PATH_IS_V1|WEB_CLIENT_FLAG_PATH_IS_V2|WEB_CLIENT_FLAG_PATH_IS_V3)
@@ -116,6 +121,14 @@ typedef enum __attribute__((packed)) {
121 #define web_client_flags_check_auth(w) web_client_flag_check(w, WEB_CLIENT_FLAG_ALL_AUTHS)
122 #define web_client_flags_clear_auth(w) web_client_flag_clear(w, WEB_CLIENT_FLAG_ALL_AUTHS)
123
124 +#define web_client_is_websocket(w) web_client_flag_check(w, WEB_CLIENT_FLAG_WEBSOCKET_CLIENT)
125 +#define web_client_set_websocket(w) web_client_flag_set(w, WEB_CLIENT_FLAG_WEBSOCKET_CLIENT)
126 +#define web_client_clear_websocket(w) web_client_flag_clear(w, WEB_CLIENT_FLAG_WEBSOCKET_CLIENT)
127 +
128 +#define web_client_has_websocket_handshake(w) web_client_flag_check(w, WEB_CLIENT_FLAG_WEBSOCKET_HANDSHAKE)
129 +#define web_client_set_websocket_handshake(w) web_client_flag_set(w, WEB_CLIENT_FLAG_WEBSOCKET_HANDSHAKE)
130 +#define web_client_clear_websocket_handshake(w) web_client_flag_clear(w, WEB_CLIENT_FLAG_WEBSOCKET_HANDSHAKE)
131 +
132 void web_client_reset_permissions(struct web_client *w);
133 void web_client_set_permissions(struct web_client *w, HTTP_ACCESS access, HTTP_USER_ROLE role, WEB_CLIENT_FLAGS auth);
134
@@ -187,6 +200,15 @@ struct web_client {
200 char *origin; // the Origin: header
201 char *user_agent; // the User-Agent: header
202
203 + // WebSocket related data - NEED TO BE FREED
204 + struct {
205 + char *key; // the Sec-WebSocket-Key header
206 + WEBSOCKET_PROTOCOL protocol; // the selected subprotocol
207 + WEBSOCKET_EXTENSION ext_flags; // bit flags for supported extensions
208 + uint8_t client_max_window_bits; // client_max_window_bits parameter (8-15)
209 + uint8_t server_max_window_bits; // server_max_window_bits parameter (8-15)
210 + } websocket;
211 +
212 BUFFER *payload; // when this request is a POST, this has the payload
213
214 NETDATA_SSL ssl;
src/web/server/web_server.c
+2
@@ -69,6 +69,8 @@ void web_server_listen_sockets_setup(void) {
69
70 if(unlikely(debug_flags & D_WEB_CLIENT))
71 debug_sockets();
72 +
73 + websocket_initialize();
74 }
75
76
src/web/websocket/autobahn-test-suite/config/fuzzingclient.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "outdir": "./reports/clients",
3 + "servers": [
4 + {
5 + "agent": "Netdata WebSocket Server",
6 + "url": "ws://localhost:19999/echo",
7 + "options": {
8 + "version": 18
9 + }
10 + }
11 + ],
12 + "cases": ["*"],
13 + "exclude-cases": [],
14 + "exclude-agent-cases": {}
15 +}
src/web/websocket/autobahn-test-suite/run-test.sh new
+15
@@ -0,0 +1,15 @@
1 +#!/usr/bin/env bash
2 +
3 +if [ ! -d "config" ]; then
4 + echo "Please create a config directory with the necessary configuration files."
5 + exit 1
6 +fi
7 +
8 +mkdir -p reports/clients
9 +
10 +docker run -it --rm \
11 + -v ${PWD}/config:/config \
12 + -v ${PWD}/reports:/reports \
13 + -p 9001:9001 \
14 + crossbario/autobahn-testsuite \
15 + wstest -m fuzzingclient -s /config/fuzzingclient.json
src/web/websocket/websocket-buffer.h new
+221
@@ -0,0 +1,221 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEBSOCKET_BUFFER_H
4 +#define NETDATA_WEBSOCKET_BUFFER_H
5 +
6 +#include "websocket-internal.h"
7 +
8 +ALWAYS_INLINE
9 +static void websocket_unmask(char *dst, const char *src, size_t length, const unsigned char *mask_key) {
10 + for (size_t i = 0; i < length; i++)
11 + dst[i] = (char)((unsigned char)src[i] ^ mask_key[i % 4]);
12 +}
13 +
14 +// Initialize an already allocated buffer structure
15 +ALWAYS_INLINE
16 +static void wsb_init(WS_BUF *wsb, size_t initial_size) {
17 + if (!wsb) return;
18 + wsb->data = mallocz(initial_size);
19 + wsb->size = initial_size;
20 + wsb->length = 0;
21 +}
22 +
23 +// Clean up an embedded buffer (free data but not the buffer structure itself)
24 +ALWAYS_INLINE
25 +static void wsb_cleanup(WS_BUF *wsb) {
26 + if (!wsb) return;
27 + freez(wsb->data);
28 + wsb->data = NULL;
29 + wsb->size = 0;
30 + wsb->length = 0;
31 +}
32 +
33 +// Initialize buffer structure
34 +ALWAYS_INLINE
35 +static WS_BUF *wsb_create(size_t initial_size) {
36 + WS_BUF *buffer = mallocz(sizeof(WS_BUF));
37 + wsb_init(buffer, MAX(initial_size, 1024));
38 + return buffer;
39 +}
40 +
41 +// Free buffer structure
42 +ALWAYS_INLINE
43 +static void wsb_free(WS_BUF *wsb) {
44 + if (!wsb) return;
45 + wsb_cleanup(wsb);
46 + freez(wsb);
47 +}
48 +
49 +// Resize buffer to a new size
50 +ALWAYS_INLINE
51 +static void wsb_resize(WS_BUF *wsb, size_t new_size) {
52 + if (new_size <= wsb->size) return;
53 + wsb->data = reallocz(wsb->data, new_size);
54 + wsb->size = new_size;
55 +}
56 +
57 +// Append data to buffer
58 +ALWAYS_INLINE
59 +static void wsb_need_bytes(WS_BUF *wsb, size_t bytes) {
60 + if (!wsb) return;
61 +
62 + // 1 for null + 4 for the final decompression padding
63 + size_t wanted_size = wsb->length + bytes + 1 + 4;
64 + if (wanted_size < wsb->size)
65 + return;
66 +
67 + size_t new_size = wsb->size * 2;
68 + if (new_size < wanted_size)
69 + new_size = wanted_size;
70 +
71 + wsb_resize(wsb, new_size);
72 +}
73 +
74 +// Reset buffer
75 +ALWAYS_INLINE
76 +static void wsb_reset(WS_BUF *wsb) {
77 + if (!wsb) return;
78 + wsb->length = 0;
79 +}
80 +
81 +// Ensure buffer has null termination for text data
82 +ALWAYS_INLINE
83 +static void wsb_null_terminate(WS_BUF *wsb) {
84 + if (!wsb) return;
85 + wsb_need_bytes(wsb, 1);
86 + wsb->data[wsb->length] = '\0';
87 +}
88 +
89 +// Check if buffer is empty
90 +ALWAYS_INLINE
91 +static bool wsb_is_empty(const WS_BUF *wsb) {
92 + return (!wsb || wsb->length == 0);
93 +}
94 +
95 +// Check if buffer has data
96 +ALWAYS_INLINE
97 +static bool wsb_has_data(const WS_BUF *wsb) {
98 + return (wsb && wsb->data && wsb->length > 0);
99 +}
100 +
101 +// Get pointer to buffer data
102 +ALWAYS_INLINE
103 +static char *wsb_data(WS_BUF *wsb) {
104 + return wsb ? wsb->data : NULL;
105 +}
106 +
107 +// Get current buffer length
108 +ALWAYS_INLINE
109 +static size_t wsb_length(const WS_BUF *wsb) {
110 + return wsb ? wsb->length : 0;
111 +}
112 +
113 +// Get allocated buffer size
114 +ALWAYS_INLINE
115 +static size_t wsb_size(const WS_BUF *wsb) {
116 + return wsb ? wsb->size : 0;
117 +}
118 +
119 +// Set buffer length (must be <= buffer size)
120 +ALWAYS_INLINE
121 +static void wsb_set_length(WS_BUF *wsb, size_t length) {
122 + if (!wsb) return;
123 +
124 + if (length > wsb->size)
125 + fatal("WEBSOCKET: trying to set length to %zu, but buffer size is %zu", length, wsb->size);
126 +
127 + wsb->length = length;
128 +}
129 +
130 +// Append data to a buffer
131 +ALWAYS_INLINE
132 +static char *wsb_append(WS_BUF *wsb, const void *data, size_t length) {
133 + if (!wsb || !data || !length)
134 + return NULL;
135 +
136 + // Ensure buffer is large enough
137 + wsb_need_bytes(wsb, length);
138 +
139 + char *dst = wsb->data + wsb->length;
140 +
141 + // Copy data to end of buffer
142 + memcpy(dst, data, length);
143 +
144 + // Update length
145 + wsb->length += length;
146 +
147 + return dst;
148 +}
149 +
150 +// Unmask and append binary data to a buffer, returns pointer to beginning of the unmasked data
151 +ALWAYS_INLINE
152 +static char *wsb_unmask_and_append(WS_BUF *wsb, const void *masked_data,
153 + size_t length, const unsigned char *mask_key) {
154 + if (!wsb || !masked_data || !length || !mask_key)
155 + return NULL;
156 +
157 + // Ensure buffer is large enough for the new data
158 + wsb_need_bytes(wsb, length);
159 +
160 + // Get a pointer to the destination in the expanded buffer
161 + char *dst = wsb->data + wsb->length;
162 +
163 + // Unmask the data directly into the buffer by calling websocket_unmask
164 + websocket_unmask(dst, (const char *)masked_data, length, mask_key);
165 +
166 + // Update buffer length
167 + wsb->length += length;
168 +
169 + return dst;
170 +}
171 +
172 +// Append data to a buffer but don't change the length (use for padding)
173 +// Returns pointer to beginning of the appended data area
174 +ALWAYS_INLINE
175 +static char *wsb_append_padding(WS_BUF *wsb, const void *data, size_t length) {
176 + if (!wsb || !data || !length)
177 + return NULL;
178 +
179 + // Ensure buffer is large enough for the new data
180 + wsb_need_bytes(wsb, length);
181 +
182 + // Get pointer to where the data will be stored
183 + char *dst = wsb->data + wsb->length;
184 +
185 + // Copy data to end of buffer
186 + memcpy(dst, data, length);
187 +
188 + // Don't update length - this is the difference from wsb_append()
189 + // This allows adding "padding" data after the logical end of the buffer
190 +
191 + return dst;
192 +}
193 +
194 +// Remove bytes from the front of the buffer, shifting remaining content forward
195 +// Returns the number of bytes actually trimmed (may be less than requested if buffer is smaller)
196 +ALWAYS_INLINE
197 +static size_t wsb_trim_front(WS_BUF *wsb, size_t bytes_to_trim) {
198 + if (!wsb || !wsb->data || bytes_to_trim == 0 || wsb->length == 0)
199 + return 0;
200 +
201 + // Cap the trim size to the actual buffer length
202 + size_t actual_trim = (bytes_to_trim > wsb->length) ? wsb->length : bytes_to_trim;
203 +
204 + if (actual_trim < wsb->length) {
205 + // More data in buffer - shift remaining data to beginning
206 + size_t remaining = wsb->length - actual_trim;
207 +
208 + // Shift the remaining data to the beginning of the buffer
209 + memmove(wsb->data, wsb->data + actual_trim, remaining);
210 +
211 + // Update buffer length to reflect the shift
212 + wsb->length = remaining;
213 + } else {
214 + // All data was trimmed or the buffer is empty - reset length
215 + wsb->length = 0;
216 + }
217 +
218 + return actual_trim;
219 +}
220 +
221 +#endif //NETDATA_WEBSOCKET_BUFFER_H
src/web/websocket/websocket-compression.c new
+248
@@ -0,0 +1,248 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-internal.h"
4 +
5 +// Initialize compression resources using the parsed options
6 +bool websocket_compression_init(WS_CLIENT *wsc) {
7 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
8 +
9 + if (!wsc->compression.enabled) {
10 + websocket_debug(wsc, "Compression is disabled");
11 + return false;
12 + }
13 +
14 + // Initialize deflate (compression) context for server-to-client messages
15 + wsc->compression.deflate_stream = mallocz(sizeof(z_stream));
16 + wsc->compression.deflate_stream->zalloc = Z_NULL;
17 + wsc->compression.deflate_stream->zfree = Z_NULL;
18 + wsc->compression.deflate_stream->opaque = Z_NULL;
19 +
20 + // Initialize with negative window bits for raw deflate (no zlib/gzip header)
21 + // Use server_max_window_bits for outgoing (server-to-client) messages
22 + int ret = deflateInit2(
23 + wsc->compression.deflate_stream,
24 + wsc->compression.compression_level,
25 + Z_DEFLATED,
26 + -wsc->compression.server_max_window_bits,
27 + WS_COMPRESS_MEMLEVEL,
28 + Z_DEFAULT_STRATEGY
29 + );
30 +
31 + if (ret != Z_OK) {
32 + websocket_error(wsc, "Failed to initialize deflate context: %s (%d)",
33 + zError(ret), ret);
34 + freez(wsc->compression.deflate_stream);
35 + wsc->compression.deflate_stream = NULL;
36 + return false;
37 + }
38 +
39 + websocket_debug(wsc, "Compression initialized (server window bits: %d)",
40 + wsc->compression.server_max_window_bits);
41 +
42 + return true;
43 +}
44 +
45 +// Initialize decompression resources for a client
46 +bool websocket_decompression_init(WS_CLIENT *wsc) {
47 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
48 +
49 + if (!wsc->compression.enabled) {
50 + websocket_debug(wsc, "Decompression is disabled");
51 + return false;
52 + }
53 +
54 + // Create a new inflate stream
55 + wsc->compression.inflate_stream = mallocz(sizeof(z_stream));
56 + wsc->compression.inflate_stream->zalloc = Z_NULL;
57 + wsc->compression.inflate_stream->zfree = Z_NULL;
58 + wsc->compression.inflate_stream->opaque = Z_NULL;
59 +
60 + // Initialize with negative window bits for raw deflate (no zlib/gzip header)
61 + // Use client_max_window_bits for incoming (client-to-server) messages
62 + int init_ret = inflateInit2(wsc->compression.inflate_stream, -wsc->compression.client_max_window_bits);
63 +
64 + if (init_ret != Z_OK) {
65 + websocket_error(wsc, "Failed to initialize inflate stream: %s (%d)",
66 + zError(init_ret), init_ret);
67 + freez(wsc->compression.inflate_stream);
68 + wsc->compression.inflate_stream = NULL;
69 + return false;
70 + }
71 +
72 + websocket_debug(wsc, "Decompression initialized (client window bits: %d)",
73 + wsc->compression.client_max_window_bits);
74 +
75 + return true;
76 +}
77 +
78 +// Clean up compression resources for a WebSocket client
79 +void websocket_compression_cleanup(WS_CLIENT *wsc) {
80 + // Clean up deflate context
81 + if (!wsc->compression.deflate_stream)
82 + return;
83 +
84 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
85 +
86 + // Set up dummy I/O pointers to ensure clean state
87 + unsigned char dummy_buffer[16] = {0};
88 + wsc->compression.deflate_stream->next_in = dummy_buffer;
89 + wsc->compression.deflate_stream->avail_in = 0;
90 + wsc->compression.deflate_stream->next_out = dummy_buffer;
91 + wsc->compression.deflate_stream->avail_out = sizeof(dummy_buffer);
92 +
93 + // Always call deflateEnd to release internal zlib resources
94 + // Don't bother with deflateReset as deflateEnd will clean up properly
95 + int ret = deflateEnd(wsc->compression.deflate_stream);
96 +
97 + if (ret != Z_OK && ret != Z_DATA_ERROR) {
98 + // Z_DATA_ERROR can happen in some edge cases, it's not critical here
99 + // as we're cleaning up anyway
100 + websocket_debug(wsc, "deflateEnd returned %d: %s", ret, zError(ret));
101 + }
102 +
103 + // Free the stream structure
104 + freez(wsc->compression.deflate_stream);
105 + wsc->compression.deflate_stream = NULL;
106 +
107 + websocket_debug(wsc, "Compression resources cleaned up");
108 +}
109 +
110 +// Clean up decompression resources for a client's inflate stream
111 +void websocket_decompression_cleanup(WS_CLIENT *wsc) {
112 + if (!wsc->compression.inflate_stream)
113 + return;
114 +
115 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
116 +
117 + // End the current inflate stream and free its resources
118 + inflateEnd(wsc->compression.inflate_stream);
119 + freez(wsc->compression.inflate_stream);
120 + wsc->compression.inflate_stream = NULL;
121 +
122 + websocket_debug(wsc, "Decompression resources cleaned up");
123 +}
124 +
125 +// Reset compression resources for a client - calls cleanup and init
126 +ALWAYS_INLINE
127 +bool websocket_compression_reset(WS_CLIENT *wsc) {
128 + websocket_compression_cleanup(wsc);
129 + return websocket_compression_init(wsc);
130 +}
131 +
132 +// Reset decompression resources for a client - calls cleanup and init
133 +ALWAYS_INLINE
134 +bool websocket_decompression_reset(WS_CLIENT *wsc) {
135 + websocket_decompression_cleanup(wsc);
136 + return websocket_decompression_init(wsc);
137 +}
138 +
139 +// Decompress a client's message from payload to u_payload
140 +bool websocket_client_decompress_message(WS_CLIENT *wsc) {
141 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
142 +
143 + if (!wsc->is_compressed || !wsc->compression.enabled || !wsc->compression.inflate_stream)
144 + return false;
145 +
146 + if (wsb_is_empty(&wsc->payload)) {
147 + websocket_debug(wsc, "Empty compressed message");
148 + wsb_reset(&wsc->u_payload);
149 + wsb_null_terminate(&wsc->u_payload);
150 + return true;
151 + }
152 +
153 + websocket_debug(wsc, "Decompressing message (%zu bytes)", wsb_length(&wsc->payload));
154 +
155 + z_stream *zstrm = wsc->compression.inflate_stream;
156 + wsb_reset(&wsc->u_payload);
157 +
158 + // Per RFC 7692, we need to append 4 bytes (00 00 FF FF) to the compressed data
159 + // to ensure the inflate operation completes
160 + static const unsigned char trailer[4] = {0x00, 0x00, 0xFF, 0xFF};
161 + wsb_append_padding(&wsc->payload, trailer, 4);
162 +
163 + zstrm->next_in = (Bytef *)wsb_data(&wsc->payload);
164 + zstrm->avail_in = wsb_length(&wsc->payload) + 4;
165 + zstrm->next_out = (Bytef *)wsb_data(&wsc->u_payload);
166 + zstrm->avail_out = wsb_size(&wsc->u_payload);
167 + zstrm->total_in = 0;
168 + zstrm->total_out = 0;
169 +
170 + // Decompress with loop for multiple buffer expansions if needed
171 + int ret = Z_MEM_ERROR;
172 + bool success = false;
173 + int retries = 24;
174 + size_t wanted_size = MAX(wsb_size(&wsc->u_payload), wsb_length(&wsc->payload) * 2);
175 + do {
176 + wsb_resize(&wsc->u_payload, wanted_size);
177 +
178 + // Position next_out to point to the end of the currently decompressed data
179 + zstrm->next_out = (Bytef *)wsb_data(&wsc->u_payload) + wsb_length(&wsc->u_payload);
180 +
181 + // Only make the newly available space available to zlib
182 + zstrm->avail_out = wsb_size(&wsc->u_payload) - wsb_length(&wsc->u_payload);
183 +
184 + // Try to decompress
185 + ret = inflate(zstrm, Z_SYNC_FLUSH);
186 +
187 + websocket_debug(wsc, "inflate() returned %d (%s), "
188 + "avail_in=%u, avail_out=%u, total_in=%lu, total_out=%lu",
189 + ret, zError(ret),
190 + zstrm->avail_in, zstrm->avail_out, zstrm->total_in, zstrm->total_out);
191 +
192 + // Handle different return codes from inflate()
193 + // Z_STREAM_END - Complete decompression success
194 + // Z_OK - Partial success, all input processed or output buffer full
195 + // Z_BUF_ERROR - Need more output space
196 +
197 + success = ret == Z_STREAM_END ||
198 + (zstrm->avail_in == 0 && zstrm->avail_out > 0 && (ret == Z_OK || ret == Z_BUF_ERROR));
199 +
200 + // Update the buffer's length to include the newly written data
201 + wsb_set_length(&wsc->u_payload, wsb_size(&wsc->u_payload) - zstrm->avail_out);
202 +
203 + // Check if we need more output space
204 + if (!success && (ret == Z_BUF_ERROR || ret == Z_OK)) {
205 + wanted_size = MIN(wanted_size * 2, WEBSOCKET_MAX_UNCOMPRESSED_SIZE);
206 + if (wanted_size == WEBSOCKET_MAX_UNCOMPRESSED_SIZE && wanted_size == wsb_size(&wsc->u_payload))
207 + break; // we cannot resize more
208 + }
209 + } while (!success && retries-- > 0);
210 +
211 + if(!success) {
212 + // Decompression failed
213 + websocket_error(wsc, "Decompression failed: %s (ret = %d, avail_in = %u)", zError(ret), ret, zstrm->avail_in);
214 + wsb_reset(&wsc->u_payload);
215 + websocket_decompression_reset(wsc);
216 + return false;
217 + }
218 +
219 + // Log successful decompression with detailed information
220 + websocket_debug(wsc, "Successfully decompressed %zu bytes to %zu bytes (ratio: %.2fx)",
221 + wsb_length(&wsc->payload), wsb_length(&wsc->u_payload),
222 + (double)wsb_length(&wsc->u_payload) / (double)wsb_length(&wsc->payload));
223 +
224 + // Show a preview of the decompressed data
225 + websocket_dump_debug(wsc, wsb_data(&wsc->u_payload), wsb_length(&wsc->u_payload), "RX UNCOMPRESSED PAYLOAD");
226 +
227 + // when client context takeover is disabled, reset the decompressor
228 + if (!wsc->compression.client_context_takeover) {
229 + websocket_debug(wsc, "resetting compression");
230 + if(inflateReset2(zstrm, -wsc->compression.client_max_window_bits) != Z_OK) {
231 + websocket_debug(wsc, "reset failed, re-initializing compression");
232 + if (!websocket_decompression_reset(wsc)) {
233 + websocket_debug(wsc, "re-initializing failed, reporting failure");
234 + return false;
235 + }
236 + zstrm = wsc->compression.inflate_stream;
237 + }
238 + }
239 +
240 + zstrm->next_in = NULL;
241 + zstrm->next_out = NULL;
242 + zstrm->avail_in = 0;
243 + zstrm->avail_out = 0;
244 + zstrm->total_in = 0;
245 + zstrm->total_out = 0;
246 +
247 + return true;
248 +}
src/web/websocket/websocket-compression.h new
+58
@@ -0,0 +1,58 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEBSOCKET_COMPRESSION_H
4 +#define NETDATA_WEBSOCKET_COMPRESSION_H
5 +
6 +#include "websocket-internal.h"
7 +
8 +// WebSocket compression constants
9 +#define WS_COMPRESS_WINDOW_BITS 15 // Default window bits (RFC 7692)
10 +#define WS_COMPRESS_MEMLEVEL 8 // Default memory level for zlib
11 +#define WS_COMPRESS_DEFAULT_LEVEL Z_DEFAULT_COMPRESSION // Default compression level
12 +#define WS_COMPRESS_MIN_SIZE 64 // Don't compress payloads smaller than this
13 +
14 +// WebSocket compression extension types
15 +typedef enum {
16 + WS_COMPRESS_NONE = 0, // No compression
17 + WS_COMPRESS_DEFLATE = 1 // permessage-deflate extension
18 +} WEBSOCKET_COMPRESSION_TYPE;
19 +
20 +// WebSocket compression context structure
21 +typedef struct websocket_compression_context {
22 + WEBSOCKET_COMPRESSION_TYPE type; // Compression type
23 + bool enabled; // Whether compression is enabled
24 + bool client_context_takeover; // Client context takeover
25 + bool server_context_takeover; // Server context takeover
26 + int client_max_window_bits; // Max window bits for client-to-server messages (8-15)
27 + int server_max_window_bits; // Max window bits for server-to-client messages (8-15)
28 + int compression_level; // Compression level
29 + z_stream *deflate_stream; // Deflate context for outgoing messages (server-to-client)
30 + z_stream *inflate_stream; // Inflate context for incoming messages (client-to-server)
31 +} WEBSOCKET_COMPRESSION_CTX;
32 +
33 +#define WEBSOCKET_COMPRESSION_DEFAULTS (WEBSOCKET_COMPRESSION_CTX){ \
34 + .type = WS_COMPRESS_NONE, \
35 + .enabled = false, \
36 + .client_context_takeover = true, \
37 + .server_context_takeover = true, \
38 + .client_max_window_bits = WS_COMPRESS_WINDOW_BITS, \
39 + .server_max_window_bits = WS_COMPRESS_WINDOW_BITS, \
40 + .compression_level = WS_COMPRESS_DEFAULT_LEVEL, \
41 + .deflate_stream = NULL, \
42 + .inflate_stream = NULL, \
43 +}
44 +
45 +// Forward declaration
46 +struct websocket_server_client;
47 +
48 +// Function declarations
49 +bool websocket_compression_init(struct websocket_server_client *wsc);
50 +void websocket_compression_cleanup(struct websocket_server_client *wsc);
51 +bool websocket_compression_reset(struct websocket_server_client *wsc);
52 +
53 +// Decompression-specific functions
54 +bool websocket_decompression_init(struct websocket_server_client *wsc);
55 +void websocket_decompression_cleanup(struct websocket_server_client *wsc);
56 +bool websocket_decompression_reset(struct websocket_server_client *wsc);
57 +
58 +#endif // NETDATA_WEBSOCKET_COMPRESSION_H
\ No newline at end of file
src/web/websocket/websocket-echo-test.html new
+1659
@@ -0,0 +1,1659 @@
1 +<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
2 +
3 +<!DOCTYPE html>
4 +<html lang="en">
5 +<head>
6 + <meta charset="UTF-8">
7 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
8 + <title>Netdata WebSocket Test</title>
9 + <style>
10 + body {
11 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
12 + max-width: 900px;
13 + margin: 0 auto;
14 + padding: 20px;
15 + line-height: 1.6;
16 + }
17 + .container {
18 + display: flex;
19 + flex-direction: column;
20 + gap: 10px;
21 + }
22 + .message-box {
23 + display: flex;
24 + gap: 10px;
25 + align-items: center;
26 + }
27 + #messageInput {
28 + flex-grow: 1;
29 + padding: 8px;
30 + border: 1px solid #ccc;
31 + border-radius: 4px;
32 + }
33 + button {
34 + padding: 8px 16px;
35 + background-color: #0078d7;
36 + color: white;
37 + border: none;
38 + border-radius: 4px;
39 + cursor: pointer;
40 + }
41 + button:hover {
42 + background-color: #0063b1;
43 + }
44 + button:disabled {
45 + background-color: #ccc;
46 + cursor: not-allowed;
47 + }
48 + #connectionStatus {
49 + padding: 8px;
50 + border-radius: 4px;
51 + margin-bottom: 10px;
52 + }
53 + .connected {
54 + background-color: #d4edda;
55 + color: #155724;
56 + }
57 + .disconnected {
58 + background-color: #f8d7da;
59 + color: #721c24;
60 + }
61 + .connecting {
62 + background-color: #fff3cd;
63 + color: #856404;
64 + }
65 + #messagesLog {
66 + border: 1px solid #ddd;
67 + padding: 10px;
68 + border-radius: 4px;
69 + max-height: 300px;
70 + overflow-y: auto;
71 + background-color: #f8f9fa;
72 + }
73 + .message {
74 + margin-bottom: 5px;
75 + padding: 5px;
76 + border-radius: 4px;
77 + }
78 + .sent {
79 + background-color: #e2f0fd;
80 + text-align: right;
81 + margin-left: 20%;
82 + }
83 + .received {
84 + background-color: #f1f1f1;
85 + margin-right: 20%;
86 + }
87 +
88 + /* Tab Styles */
89 + .tabs {
90 + display: flex;
91 + border-bottom: 1px solid #ddd;
92 + margin-bottom: 15px;
93 + }
94 + .tab {
95 + padding: 10px 20px;
96 + cursor: pointer;
97 + border: 1px solid transparent;
98 + border-bottom: none;
99 + margin-right: 5px;
100 + border-radius: 4px 4px 0 0;
101 + }
102 + .tab.active {
103 + border-color: #ddd;
104 + background-color: #fff;
105 + border-bottom: 1px solid #fff;
106 + margin-bottom: -1px;
107 + font-weight: bold;
108 + }
109 + .tab-content {
110 + display: none;
111 + }
112 + .tab-content.active {
113 + display: block;
114 + }
115 +
116 + /* Progress Bar */
117 + .progress-container {
118 + width: 100%;
119 + height: 20px;
120 + background-color: #f1f1f1;
121 + border-radius: 4px;
122 + margin: 10px 0;
123 + }
124 + .progress-bar {
125 + height: 100%;
126 + background-color: #4caf50;
127 + border-radius: 4px;
128 + width: 0%;
129 + transition: width 0.5s ease;
130 + }
131 +
132 + /* Stats Panel */
133 + .stats-panel {
134 + display: grid;
135 + grid-template-columns: repeat(3, 1fr);
136 + gap: 10px;
137 + margin: 15px 0;
138 + }
139 + .stat-card {
140 + background-color: #f8f9fa;
141 + border: 1px solid #ddd;
142 + border-radius: 4px;
143 + padding: 10px;
144 + text-align: center;
145 + }
146 + .stat-card.error {
147 + background-color: #f8d7da;
148 + border-color: #f5c6cb;
149 + }
150 + .stat-value {
151 + font-size: 1.5em;
152 + font-weight: bold;
153 + margin: 5px 0;
154 + }
155 + .stat-label {
156 + font-size: 0.85em;
157 + color: #666;
158 + }
159 +
160 + /* Form Controls for Stress Test */
161 + .form-group {
162 + margin-bottom: 15px;
163 + }
164 + label {
165 + display: inline-block;
166 + margin-bottom: 5px;
167 + font-weight: bold;
168 + }
169 + input[type="number"], input[type="range"] {
170 + width: 100%;
171 + padding: 8px;
172 + box-sizing: border-box;
173 + border: 1px solid #ccc;
174 + border-radius: 4px;
175 + }
176 +
177 + /* Log Area */
178 + #stressTestLog {
179 + border: 1px solid #ddd;
180 + padding: 10px;
181 + border-radius: 4px;
182 + max-height: 200px;
183 + overflow-y: auto;
184 + background-color: #f8f9fa;
185 + font-family: monospace;
186 + margin-top: 10px;
187 + }
188 + </style>
189 +</head>
190 +<body>
191 + <h1>Netdata WebSocket Test Client</h1>
192 +
193 + <!-- Common Connection Controls -->
194 + <div class="container">
195 + <div id="connectionStatus" class="disconnected">Disconnected</div>
196 +
197 + <div class="message-box">
198 + <label for="endpointInput">WebSocket URL:</label>
199 + <input type="text" id="endpointInput" value="ws://localhost:19999/echo" style="flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
200 + <button id="toggleProtocolButton" title="Toggle between ws:// and wss://">Switch to wss://</button>
201 + </div>
202 +
203 + <div class="message-box">
204 + <input type="checkbox" id="enableDebugMode">
205 + <label for="enableDebugMode">Debug Mode (more verbose logging)</label>
206 + </div>
207 +
208 + <div class="message-box">
209 + <button id="connectButton">Connect</button>
210 + <button id="disconnectButton" disabled>Disconnect</button>
211 + </div>
212 +
213 + <div id="connectionDetails" style="margin-top: 10px; display: none; background-color: #e7f3fe; padding: 10px; border-radius: 4px;">
214 + <div><strong>Protocol:</strong> <span id="negotiatedProtocol">-</span></div>
215 + <div><strong>Extensions:</strong> <span id="negotiatedExtensions">-</span></div>
216 + <div><strong>Compression:</strong> <span id="compressionStatus">Disabled</span></div>
217 + </div>
218 + </div>
219 +
220 + <!-- Tab Navigation -->
221 + <div class="tabs">
222 + <div class="tab active" data-tab="basicTest">Basic Testing</div>
223 + <div class="tab" data-tab="stressTest">Stress Test</div>
224 + </div>
225 +
226 + <!-- Basic Test Tab -->
227 + <div id="basicTest" class="tab-content active">
228 + <div class="container">
229 + <div class="message-box">
230 + <input type="text" id="messageInput" placeholder="Type a message to send..." disabled>
231 + <button id="sendButton" disabled>Send</button>
232 + <button id="testLargeMessageButton" disabled>Test Compression</button>
233 + </div>
234 +
235 + <div id="compressionOptions" style="margin-top: 10px; display: none; background-color: #f8f9fa; padding: 10px; border-radius: 4px;">
236 + <div style="margin-bottom: 10px;"><strong>Message Compression Characteristics:</strong></div>
237 +
238 + <div style="display: flex; gap: 15px; margin-bottom: 10px;">
239 + <label>
240 + <input type="radio" name="compressionType" value="high" checked>
241 + Highly Compressible
242 + </label>
243 +
244 + <label>
245 + <input type="radio" name="compressionType" value="medium">
246 + Medium Compression
247 + </label>
248 +
249 + <label>
250 + <input type="radio" name="compressionType" value="low">
251 + Low Compression
252 + </label>
253 +
254 + <label>
255 + <input type="radio" name="compressionType" value="mixed">
256 + Mixed Content
257 + </label>
258 + </div>
259 +
260 + <div style="margin-top: 10px;">
261 + <label style="display: flex; align-items: center; cursor: pointer;">
262 + <input type="checkbox" id="useBinaryMode" style="margin-right: 8px;">
263 + <span>Send as Binary Frame (use for non-UTF8 content)</span>
264 + </label>
265 + <div style="font-size: 0.85em; color: #666; margin-top: 5px; margin-left: 24px;">
266 + Use binary mode only when sending data that is not valid UTF-8 text.
267 + For most test cases, text mode works fine.
268 + </div>
269 + </div>
270 + </div>
271 +
272 + <h3>Messages</h3>
273 + <div id="messagesLog"></div>
274 + </div>
275 + </div>
276 +
277 + <!-- Stress Test Tab -->
278 + <div id="stressTest" class="tab-content">
279 + <div class="container">
280 + <h3>WebSocket Stress Tester</h3>
281 + <p>Configure the stress test parameters below. This will send random messages for the specified duration and verify responses.</p>
282 +
283 + <div class="form-group">
284 + <label for="testDurationMinutes">Test Duration (minutes):</label>
285 + <input type="number" id="testDurationMinutes" value="1" min="0.1" max="60" step="0.1">
286 + </div>
287 +
288 + <div class="form-group">
289 + <label for="minMessageSize">Minimum Message Size (bytes):</label>
290 + <input type="number" id="minMessageSize" value="100" min="10" max="50000">
291 + </div>
292 +
293 + <div class="form-group">
294 + <label for="maxMessageSize">Maximum Message Size (bytes):</label>
295 + <input type="number" id="maxMessageSize" value="10000" min="100" max="100000">
296 + </div>
297 +
298 + <div class="form-group">
299 + <label for="singleSize">Use Fixed Message Size:</label>
300 + <input type="checkbox" id="useSingleSize" style="margin-left: 10px;">
301 + <input type="number" id="singleMessageSize" value="5000" min="100" max="100000" style="width: 120px; margin-left: 10px;">
302 + <span style="font-size: 0.85em; color: #666; margin-left: 10px;">Check to use a consistent message size for better testing</span>
303 + </div>
304 +
305 + <div class="form-group">
306 + <label for="messageFrequency">Messages per Second:</label>
307 + <input type="range" id="messageFrequency" value="5" min="1" max="100" step="1">
308 + <div style="display: flex; justify-content: space-between;">
309 + <span>1</span>
310 + <span id="messageFrequencyValue">5</span>
311 + <span>100</span>
312 + </div>
313 + <div style="font-size: 0.85em; color: #0c5460; font-weight: normal; background-color: #d1ecf1; padding: 5px; border-radius: 4px; margin-top: 5px;">
314 + NOTE: Maximum rate is limited to 100 messages/second for reliable testing,
315 + as browsers cannot reliably process WebSocket messages at higher rates.
316 + </div>
317 + </div>
318 +
319 + <div class="message-box">
320 + <button id="startStressTestButton" disabled>Start Stress Test</button>
321 + <button id="stopStressTestButton" disabled>Stop Test</button>
322 + </div>
323 +
324 + <div class="progress-container">
325 + <div id="stressTestProgress" class="progress-bar" style="width: 0%"></div>
326 + </div>
327 +
328 + <div id="timeRemaining" style="text-align: center;">Ready to start</div>
329 +
330 + <!-- Real-time Stats -->
331 + <div class="stats-panel">
332 + <div class="stat-card">
333 + <div class="stat-value" id="messagesSent">0</div>
334 + <div class="stat-label">Messages Sent</div>
335 + </div>
336 + <div class="stat-card">
337 + <div class="stat-value" id="messagesReceived">0</div>
338 + <div class="stat-label">Messages Received</div>
339 + </div>
340 + <div class="stat-card" id="errorCard">
341 + <div class="stat-value" id="errorCount">0</div>
342 + <div class="stat-label">Errors</div>
343 + </div>
344 + <div class="stat-card">
345 + <div class="stat-value" id="avgLatency">0 ms</div>
346 + <div class="stat-label">Average Latency</div>
347 + </div>
348 + <div class="stat-card">
349 + <div class="stat-value" id="bytesSent">0 KB</div>
350 + <div class="stat-label">Data Sent</div>
351 + </div>
352 + <div class="stat-card">
353 + <div class="stat-value" id="bytesReceived">0 KB</div>
354 + <div class="stat-label">Data Received</div>
355 + </div>
356 + </div>
357 +
358 + <h4>Test Log</h4>
359 + <div id="stressTestLog"></div>
360 + </div>
361 + </div>
362 +
363 + <script>
364 + // Tab switching functionality
365 + document.querySelectorAll('.tab').forEach(tab => {
366 + tab.addEventListener('click', () => {
367 + // Remove active class from all tabs and content
368 + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
369 + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
370 +
371 + // Add active class to clicked tab and its content
372 + tab.classList.add('active');
373 + const tabContentId = tab.getAttribute('data-tab');
374 + document.getElementById(tabContentId).classList.add('active');
375 + });
376 + });
377 +
378 + // Elements - Common
379 + const connectButton = document.getElementById('connectButton');
380 + const disconnectButton = document.getElementById('disconnectButton');
381 + const connectionStatus = document.getElementById('connectionStatus');
382 + const endpointInput = document.getElementById('endpointInput');
383 + const toggleProtocolButton = document.getElementById('toggleProtocolButton');
384 + const enableCompression = { checked: true }; // Default to enabled
385 + const enableDebugMode = document.getElementById('enableDebugMode');
386 + const connectionDetails = document.getElementById('connectionDetails');
387 + const negotiatedProtocol = document.getElementById('negotiatedProtocol');
388 + const negotiatedExtensions = document.getElementById('negotiatedExtensions');
389 + const compressionStatus = document.getElementById('compressionStatus');
390 +
391 + // Elements - Basic Test Tab
392 + const messageInput = document.getElementById('messageInput');
393 + const sendButton = document.getElementById('sendButton');
394 + const testLargeMessageButton = document.getElementById('testLargeMessageButton');
395 + const messagesLog = document.getElementById('messagesLog');
396 +
397 + // Elements - Stress Test Tab
398 + const startStressTestButton = document.getElementById('startStressTestButton');
399 + const stopStressTestButton = document.getElementById('stopStressTestButton');
400 + const testDurationMinutes = document.getElementById('testDurationMinutes');
401 + const minMessageSize = document.getElementById('minMessageSize');
402 + const maxMessageSize = document.getElementById('maxMessageSize');
403 + const useSingleSize = document.getElementById('useSingleSize');
404 + const singleMessageSize = document.getElementById('singleMessageSize');
405 + const messageFrequency = document.getElementById('messageFrequency');
406 + const messageFrequencyValue = document.getElementById('messageFrequencyValue');
407 + const stressTestProgress = document.getElementById('stressTestProgress');
408 + const timeRemaining = document.getElementById('timeRemaining');
409 + const stressTestLog = document.getElementById('stressTestLog');
410 + const errorCard = document.getElementById('errorCard');
411 +
412 + // Stats elements
413 + const messagesSentElement = document.getElementById('messagesSent');
414 + const messagesReceivedElement = document.getElementById('messagesReceived');
415 + const errorCountElement = document.getElementById('errorCount');
416 + const avgLatencyElement = document.getElementById('avgLatency');
417 + const bytesSentElement = document.getElementById('bytesSent');
418 + const bytesReceivedElement = document.getElementById('bytesReceived');
419 +
420 + // Update frequency slider value display
421 + messageFrequency.addEventListener('input', () => {
422 + messageFrequencyValue.textContent = messageFrequency.value;
423 + });
424 +
425 + // WebSocket connection
426 + let socket = null;
427 +
428 + // Connect to the WebSocket server
429 + connectButton.addEventListener('click', () => {
430 + // Get the URL from the input field
431 + const url = endpointInput.value.trim();
432 + if (!url) {
433 + alert("Please enter a valid WebSocket URL");
434 + return;
435 + }
436 +
437 + // Validate the URL has a WebSocket protocol
438 + if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
439 + alert("WebSocket URL must start with ws:// or wss://");
440 + return;
441 + }
442 +
443 + connectionStatus.textContent = 'Connecting to ' + url;
444 + connectionStatus.className = 'connecting';
445 + connectionDetails.style.display = 'none';
446 + addMessageToLog('Connecting to: ' + url, 'system');
447 +
448 + try {
449 + // Create WebSocket connection with netdata-json protocol
450 + const protocols = [];
451 + socket = new WebSocket(url, protocols);
452 + addMessageToLog('WebSocket connection created', 'system');
453 +
454 + // Connection opened
455 + socket.addEventListener('open', (event) => {
456 + connectionStatus.textContent = 'Connected';
457 + connectionStatus.className = 'connected';
458 +
459 + // Enable all connection-dependent controls
460 + connectButton.disabled = true;
461 + disconnectButton.disabled = false;
462 + messageInput.disabled = false;
463 + sendButton.disabled = false;
464 + testLargeMessageButton.disabled = false;
465 + startStressTestButton.disabled = false;
466 +
467 + // Display connection details
468 + connectionDetails.style.display = 'block';
469 + negotiatedProtocol.textContent = socket.protocol || 'none';
470 +
471 + const extensions = socket.extensions || 'none';
472 + negotiatedExtensions.textContent = extensions;
473 +
474 + // Check if compression was negotiated
475 + if (extensions.includes('permessage-deflate')) {
476 + compressionStatus.textContent = 'Enabled (permessage-deflate)';
477 + compressionStatus.style.color = '#155724';
478 + } else {
479 + compressionStatus.textContent = 'Disabled';
480 + compressionStatus.style.color = '#721c24';
481 + }
482 +
483 + addMessageToLog(`Connection established (Protocol: ${socket.protocol}, Extensions: ${extensions})`, 'system');
484 + });
485 +
486 + // Track message size for network analysis
487 + let lastMessageSentInfo = null;
488 +
489 + // Listen for messages
490 + socket.addEventListener('message', (event) => {
491 + const message = event.data;
492 + const receiveTime = performance.now();
493 +
494 + // First check if this is a stress test message
495 + if (stressTestRunning && typeof message === 'string' && message.startsWith('MSG-')) {
496 + const handled = processStressTestResponse(message);
497 + if (handled) return; // Skip normal message processing if it was a stress test message
498 + }
499 +
500 + // Get transfer size from event if available
501 + let transferSize = 0;
502 +
503 + // Check if this is a response to a benchmark test
504 + if (window.currentBenchmark && !window.currentBenchmark.responseComplete) {
505 + // Update benchmark data
506 + const benchmark = window.currentBenchmark;
507 +
508 + if (!benchmark.lastResponseTime) {
509 + // First response packet
510 + benchmark.lastResponseTime = receiveTime;
511 + const firstPacketLatency = (receiveTime - benchmark.sendTime).toFixed(2);
512 + addMessageToLog(`First response received after ${firstPacketLatency}ms`, 'system');
513 + }
514 +
515 + // Handle different data types (string or blob)
516 + const processResponse = (responseData) => {
517 + // Track bytes received
518 + benchmark.receivedBytes = responseData.length;
519 +
520 + // Check if response is equal to what we sent
521 + // For compressed responses, the protocol should preserve the exact content
522 + const dataMatches = (responseData === benchmark.originalMessage);
523 +
524 + // Check if response seems complete - always do the verification
525 + // regardless of the content to ensure we check data integrity
526 + if (responseData && responseData.length > 0) {
527 + benchmark.responseComplete = true;
528 + const totalTime = (receiveTime - benchmark.sendTime).toFixed(2);
529 +
530 + // Data verification results
531 + let verificationResults = '';
532 + if (dataMatches) {
533 + verificationResults = `<div style="color: #155724; margin-top: 5px;"><strong>✓ Data Integrity:</strong> Response content matches sent data exactly!</div>`;
534 + } else {
535 + // If sizes match but content differs
536 + if (responseData.length === benchmark.originalMessage.length) {
537 + verificationResults = `<div style="color: #721c24; margin-top: 5px;"><strong>❌ Data Integrity:</strong> Response size matches (${responseData.length} bytes) but content differs!</div>`;
538 +
539 + // Add some diagnostic info to help understand the mismatch
540 + // Find the first point of difference
541 + let diffPos = -1;
542 + for (let i = 0; i < responseData.length; i++) {
543 + if (responseData[i] !== benchmark.originalMessage[i]) {
544 + diffPos = i;
545 + break;
546 + }
547 + }
548 +
549 + if (diffPos >= 0) {
550 + const sentContext = benchmark.originalMessage.substring(
551 + Math.max(0, diffPos - 10),
552 + Math.min(benchmark.originalMessage.length, diffPos + 10)
553 + );
554 + const recvContext = responseData.substring(
555 + Math.max(0, diffPos - 10),
556 + Math.min(responseData.length, diffPos + 10)
557 + );
558 +
559 + verificationResults += `<div style="color: #721c24; margin-top: 5px;">
560 + First difference at position ${diffPos}:<br>
561 + Sent: "${sentContext}" (char code: ${benchmark.originalMessage.charCodeAt(diffPos)})<br>
562 + Recv: "${recvContext}" (char code: ${responseData.charCodeAt(diffPos)})
563 + </div>`;
564 + }
565 + } else {
566 + verificationResults = `<div style="color: #721c24; margin-top: 5px;"><strong>❌ Data Integrity:</strong> Response content differs! Sent: ${benchmark.originalMessage.length} bytes, Received: ${responseData.length} bytes</div>`;
567 + }
568 + }
569 +
570 + // Calculate compression ratio if we know the original size
571 + let compressionInfo = "";
572 + if (benchmark.expectedBytes > 0 && responseData.length > 0) {
573 + // Check if compression extension was actually negotiated
574 + const usingCompression = negotiatedExtensions.textContent.includes("permessage-deflate");
575 +
576 + const compressionRatio = (benchmark.expectedBytes / responseData.length).toFixed(2);
577 + const percentReduction = ((1 - (responseData.length / benchmark.expectedBytes)) * 100).toFixed(2);
578 +
579 + // Use color formatting based on compression effectiveness
580 + const compressionColor = percentReduction > 50 ? '#155724' : (percentReduction > 20 ? '#856404' : '#721c24');
581 +
582 + compressionInfo = `
583 + <div style="margin-top: 5px;">
584 + <strong>Compression Stats:</strong>
585 + <ul style="margin-top: 5px; margin-bottom: 5px;">
586 + <li>Original size: ${(benchmark.expectedBytes / 1024).toFixed(2)} KB</li>
587 + <li>Size in JavaScript: ${(responseData.length / 1024).toFixed(2)} KB <span style="color: #856404">(Browser auto-decompressed the data)</span></li>
588 + <li>WebSocket extensions: ${usingCompression ?
589 + '<span style="color: #155724">permessage-deflate enabled (compression happens at protocol level)</span>' :
590 + '<span style="color: #721c24">compression not enabled! Check server configuration.</span>'}</li>
591 + <li style="color: #155724">Note: The server is correctly compressing the data (99.5% reduction), but the browser automatically decompresses it before JavaScript receives it</li>
592 + </ul>
593 + </div>
594 + `;
595 + }
596 +
597 + // Calculate throughput
598 + const kbPerSecond = ((benchmark.receivedBytes / 1024) / (totalTime / 1000)).toFixed(2);
599 +
600 + addMessageToLog(
601 + `Benchmark complete: ${benchmark.messageSizeKB} KB message echoed back in ${totalTime}ms (${kbPerSecond} KB/s)${verificationResults}${compressionInfo}`,
602 + 'system',
603 + true
604 + );
605 +
606 + // Store this message info for display
607 + lastMessageSentInfo = {
608 + sent: benchmark.expectedBytes,
609 + received: responseData.length,
610 + dataMatches: dataMatches
611 + };
612 +
613 + // Reset benchmark
614 + window.currentBenchmark = null;
615 + }
616 + };
617 +
618 + // Check if the response is a Blob (binary data)
619 + if (message instanceof Blob) {
620 + addMessageToLog(`Received response as BINARY data (${message.size} bytes)`, 'system');
621 +
622 + // Handle binary data by reading it as text for comparison
623 + const reader = new FileReader();
624 + reader.onload = function() {
625 + // Convert binary to text for comparison
626 + const responseText = reader.result;
627 + addMessageToLog(`Successfully converted binary response to text for comparison (${responseText.length} chars)`, 'system');
628 + processResponse(responseText);
629 + };
630 + reader.onerror = function() {
631 + addMessageToLog(`Error: Failed to read binary response data for verification`, 'system');
632 + // Still try to process what we can
633 + benchmark.responseComplete = true;
634 + const totalTime = (receiveTime - benchmark.sendTime).toFixed(2);
635 + addMessageToLog(
636 + `Benchmark complete: ${benchmark.messageSizeKB} KB message echoed back in ${totalTime}ms as binary data, but verification failed`,
637 + 'system',
638 + true
639 + );
640 + };
641 + reader.readAsText(message);
642 + } else {
643 + // It's already text
644 + addMessageToLog(`Received response as TEXT data (${message.length} chars)`, 'system');
645 + processResponse(message);
646 + }
647 + }
648 +
649 + // Add compression info to received message if available
650 + let receivedInfo = '';
651 + if (lastMessageSentInfo && lastMessageSentInfo.sent > 0) {
652 + // For binary blobs we use the size property, for string we use length
653 + const receivedLength = message instanceof Blob ? message.size : message.length;
654 +
655 + if (receivedLength > 0 && receivedLength !== lastMessageSentInfo.sent) {
656 + const compressionRatio = (lastMessageSentInfo.sent / receivedLength).toFixed(2);
657 + const percentReduction = ((1 - (receivedLength / lastMessageSentInfo.sent)) * 100).toFixed(2);
658 +
659 + receivedInfo = ` | Compressed: ${(receivedLength / 1024).toFixed(2)} KB (${percentReduction}% smaller than sent)`;
660 + }
661 +
662 + // Add data integrity info if available
663 + if (lastMessageSentInfo.hasOwnProperty('dataMatches')) {
664 + receivedInfo += ` | Data Integrity: ${lastMessageSentInfo.dataMatches ? '✓ Match' : '❌ Mismatch'}`;
665 + }
666 +
667 + // Reset after use
668 + lastMessageSentInfo = null;
669 + }
670 +
671 + // Log the message - if it's a blob, show that in a friendly way
672 + if (message instanceof Blob) {
673 + addMessageToLog(`[Binary data: ${message.size} bytes]`, 'received', false, receivedInfo);
674 + } else {
675 + addMessageToLog(message, 'received', false, receivedInfo);
676 + }
677 + });
678 +
679 + // Connection closed
680 + socket.addEventListener('close', (event) => {
681 + connectionStatus.textContent = 'Disconnected';
682 + connectionStatus.className = 'disconnected';
683 + connectionDetails.style.display = 'none';
684 +
685 + // Disable all connection-dependent controls
686 + connectButton.disabled = false;
687 + disconnectButton.disabled = true;
688 + messageInput.disabled = true;
689 + sendButton.disabled = true;
690 + testLargeMessageButton.disabled = true;
691 + startStressTestButton.disabled = true;
692 +
693 + // Stop any running stress test
694 + if (stressTestRunning) {
695 + stopStressTest();
696 + addStressTestLog('Test stopped due to WebSocket disconnection', 'error');
697 + }
698 +
699 + let closeReason = '';
700 + let recommendedAction = '';
701 +
702 + // Try to give a friendly description of common close codes
703 + switch (event.code) {
704 + case 1000:
705 + closeReason = "Normal closure";
706 + recommendedAction = "This is a clean shutdown, no action needed.";
707 + break;
708 + case 1001:
709 + closeReason = "Going away";
710 + recommendedAction = "The server or browser is navigating away from the page.";
711 + break;
712 + case 1002:
713 + closeReason = "Protocol error";
714 + recommendedAction = "Check the messages sent - may indicate a problem with message format or headers.";
715 + break;
716 + case 1003:
717 + closeReason = "Unsupported data";
718 + recommendedAction = "The server couldn't process the data type sent.";
719 + break;
720 + case 1005:
721 + closeReason = "No status code";
722 + recommendedAction = "Connection closed without a proper code (abnormal).";
723 + break;
724 + case 1006:
725 + closeReason = "Abnormal closure";
726 + recommendedAction = "Connection was closed unexpectedly. Check server logs or network connectivity.";
727 + break;
728 + case 1007:
729 + closeReason = "Invalid frame payload data";
730 + recommendedAction = "Message contained invalid data format, possibly not valid UTF-8 text.";
731 + break;
732 + case 1008:
733 + closeReason = "Policy violation";
734 + recommendedAction = "Server policy was violated. Check message rate or authentication.";
735 + break;
736 + case 1009:
737 + closeReason = "Message too big";
738 + recommendedAction = "Try reducing your message size or using fragmentation.";
739 + break;
740 + case 1010:
741 + closeReason = "Missing extension";
742 + recommendedAction = "Client requested an extension the server doesn't support.";
743 + break;
744 + case 1011:
745 + closeReason = "Internal error";
746 + recommendedAction = "Server encountered an unexpected error. Check server logs.";
747 + break;
748 + case 1012:
749 + closeReason = "Service restart";
750 + recommendedAction = "The server is restarting, try reconnecting in a moment.";
751 + break;
752 + case 1013:
753 + closeReason = "Try again later";
754 + recommendedAction = "Server is temporarily unavailable, try reconnecting later.";
755 + break;
756 + case 1014:
757 + closeReason = "Bad gateway";
758 + recommendedAction = "A gateway or proxy received an invalid response from the upstream server.";
759 + break;
760 + case 1015:
761 + closeReason = "TLS handshake failure";
762 + recommendedAction = "Check your SSL/TLS configuration and certificates.";
763 + break;
764 + // Netdata specific codes (4000+)
765 + case 4000:
766 + closeReason = "Netdata: Client timeout";
767 + recommendedAction = "The connection was inactive for too long.";
768 + break;
769 + case 4001:
770 + closeReason = "Netdata: Server shutdown";
771 + recommendedAction = "The Netdata server is shutting down.";
772 + break;
773 + case 4002:
774 + closeReason = "Netdata: Connection rejected";
775 + recommendedAction = "The server rejected the connection (check authorization).";
776 + break;
777 + case 4003:
778 + closeReason = "Netdata: Rate limit exceeded";
779 + recommendedAction = "You've exceeded the message rate limit. Reduce message frequency.";
780 + break;
781 + default:
782 + closeReason = "Unknown";
783 + recommendedAction = "Unrecognized close code - check server logs for details.";
784 + break;
785 + }
786 +
787 + // Add detailed information to the log
788 + let closeMessage = `Connection closed (code: ${event.code} - ${closeReason})`;
789 + if (event.reason) {
790 + closeMessage += `\nReason: ${event.reason}`;
791 + }
792 +
793 + // Add recommendation if available
794 + if (recommendedAction) {
795 + closeMessage += `\nRecommended action: ${recommendedAction}`;
796 + }
797 +
798 + // Check for common error conditions
799 + if (event.code === 1006) {
800 + // Add more detailed debugging advice for abnormal closures
801 + closeMessage += "\n\nThis error often occurs when:";
802 + closeMessage += "\n- The server crashed or was stopped";
803 + closeMessage += "\n- Network connectivity issues occurred";
804 + closeMessage += "\n- A browser timeout occurred due to inactivity";
805 + closeMessage += "\n- Invalid WebSocket headers were sent";
806 + } else if (event.code === 1009) {
807 + // For message too big errors
808 + closeMessage += "\n\nTo fix message size issues:";
809 + closeMessage += "\n- Break large messages into smaller chunks";
810 + closeMessage += "\n- Consider using binary mode for large messages";
811 + closeMessage += "\n- Check the server's max message size configuration";
812 + }
813 +
814 + addMessageToLog(closeMessage, 'system');
815 +
816 + // If we have pending messages during disconnect, log the details
817 + if (pendingMessages.size > 0) {
818 + addMessageToLog(`Warning: ${pendingMessages.size} messages were still pending when the connection closed`, 'system');
819 + }
820 + });
821 +
822 + // Connection error
823 + socket.addEventListener('error', (error) => {
824 + connectionStatus.textContent = 'Connection Error';
825 + connectionStatus.className = 'disconnected';
826 + connectionDetails.style.display = 'none';
827 +
828 + // Stop any running stress test
829 + if (stressTestRunning) {
830 + stopStressTest();
831 + addStressTestLog('Test stopped due to WebSocket error', 'error');
832 + }
833 +
834 + // Log details about the error
835 + const errorDetails = `WebSocket error occurred during ${stressTestRunning ? 'stress test' : 'normal operation'}`;
836 + addMessageToLog(errorDetails, 'system');
837 +
838 + // Check the type of error and provide more helpful information
839 + if (error instanceof Event && error.target) {
840 + // WebSocket error events don't contain much useful information
841 + // Add troubleshooting advice
842 + let troubleshooting = "\nPossible reasons for WebSocket errors:";
843 + troubleshooting += "\n- Network connectivity issues";
844 + troubleshooting += "\n- Server unavailable or incorrect URL";
845 + troubleshooting += "\n- Cross-origin issues (CORS)";
846 + troubleshooting += "\n- Too many simultaneous connections";
847 +
848 + // Add specific advice for stress testing
849 + if (stressTestRunning) {
850 + troubleshooting += "\n\nFor stress test issues:";
851 + troubleshooting += "\n- Reduce message frequency";
852 + troubleshooting += "\n- Reduce message size";
853 + troubleshooting += "\n- Check for 'invalid frame header' errors in the browser console";
854 + troubleshooting += "\n- Try using smaller message bursts";
855 + }
856 +
857 + addMessageToLog(troubleshooting, 'system');
858 + }
859 +
860 + addMessageToLog('Check the browser console (F12 > Console tab) for more error details', 'system');
861 + console.error('WebSocket error:', error);
862 + });
863 + } catch (error) {
864 + connectionStatus.textContent = 'Connection Failed';
865 + connectionStatus.className = 'disconnected';
866 + console.error('Failed to create WebSocket:', error);
867 + }
868 + });
869 +
870 + // Disconnect from the WebSocket server
871 + disconnectButton.addEventListener('click', () => {
872 + if (socket) {
873 + socket.close();
874 + socket = null;
875 + }
876 + });
877 +
878 + // Send message
879 + sendButton.addEventListener('click', sendMessage);
880 + messageInput.addEventListener('keypress', (event) => {
881 + if (event.key === 'Enter') {
882 + sendMessage();
883 + }
884 + });
885 +
886 + // Show compression options when hovering over test button
887 + testLargeMessageButton.addEventListener('mouseenter', () => {
888 + document.getElementById('compressionOptions').style.display = 'block';
889 + });
890 +
891 + // Test compression with large message
892 + testLargeMessageButton.addEventListener('click', () => {
893 + if (socket && socket.readyState === WebSocket.OPEN) {
894 + // Prompt for message size
895 + const sizePrompt = prompt("Enter approximate message size in KB (10-1000):", "100");
896 + if (!sizePrompt) return;
897 +
898 + const sizeKB = parseInt(sizePrompt);
899 + if (isNaN(sizeKB) || sizeKB < 10 || sizeKB > 1000) {
900 + alert("Please enter a valid size between 10 and 1000 KB");
901 + return;
902 + }
903 +
904 + // Create a large message with realistic data that has varying compression characteristics
905 + // This will be a better test of real-world compression performance
906 +
907 + // Random compression efficiency for this test
908 + const compressionType = document.querySelector('input[name="compressionType"]:checked')?.value || 'mixed';
909 +
910 + // Calculate target size in bytes
911 + const targetBytes = sizeKB * 1024;
912 +
913 + // Whether to send as binary (true) or text (false)
914 + // For the test page specifically, we're generating text data
915 + // We'll only use binary mode for demonstration if explicitly selected by user
916 + let sendAsBinary = false;
917 +
918 + // Add UI to let user choose text/binary mode
919 + const binaryModeCheckbox = document.getElementById('useBinaryMode');
920 + if (binaryModeCheckbox && binaryModeCheckbox.checked) {
921 + sendAsBinary = true;
922 + }
923 +
924 + // Create the message based on compression type selected
925 + let largeMessage = "This is a test message to verify WebSocket compression. ";
926 +
927 + switch (compressionType) {
928 + case 'high':
929 + // Highly compressible - repeating text blocks
930 + const baseText = "This is a highly compressible repeating pattern. ";
931 + while (largeMessage.length < targetBytes) {
932 + largeMessage += baseText;
933 + }
934 + break;
935 +
936 + case 'medium':
937 + // Medium compressibility - structured data with some repetition
938 + // This is valid UTF-8 JSON-like data that compresses moderately well
939 + while (largeMessage.length < targetBytes) {
940 + largeMessage += `{"id":${Math.floor(Math.random() * 1000)},"name":"user${Math.floor(Math.random() * 100)}","timestamp":${Date.now()},"value":${Math.random()},"status":"active"},`;
941 + }
942 + break;
943 +
944 + case 'low':
945 + // Low compressibility - random data
946 + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
947 + while (largeMessage.length < targetBytes) {
948 + largeMessage += chars.charAt(Math.floor(Math.random() * chars.length));
949 + }
950 + break;
951 +
952 + case 'mixed':
953 + default:
954 + // Define character set for random data (also defined in 'low' case)
955 + const mixedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
956 +
957 + // Mixed - combination of different patterns
958 + const sections = [
959 + // Highly compressible section (20%)
960 + Array(Math.floor(targetBytes * 0.2 / 50)).fill("AAAABBBBBCCCCCDDDDDEEEEEFFFFF").join(""),
961 +
962 + // Moderately compressible section (40%)
963 + Array(Math.floor(targetBytes * 0.4 / 100)).fill().map(() =>
964 + `{"id":${Math.floor(Math.random() * 100)},"data":"value"}`
965 + ).join(","),
966 +
967 + // Barely compressible section (40%)
968 + Array(Math.floor(targetBytes * 0.4)).fill().map(() =>
969 + mixedChars.charAt(Math.floor(Math.random() * mixedChars.length))
970 + ).join("")
971 + ];
972 +
973 + largeMessage += sections.join("");
974 + break;
975 + }
976 +
977 + // Trim to exact size if needed
978 + if (largeMessage.length > targetBytes) {
979 + largeMessage = largeMessage.substring(0, targetBytes);
980 + }
981 +
982 + // Show message size
983 + const actualSizeKB = (largeMessage.length / 1024).toFixed(2);
984 + addMessageToLog(`Sending large message (${actualSizeKB} KB) to test compression...`, 'system');
985 +
986 + // Measure time before sending
987 + const startTime = performance.now();
988 +
989 + // Send the message explicitly as a BINARY frame for large compressed data
990 + // This prevents UTF-8 validation issues with random/compressed data
991 + if (sendAsBinary) {
992 + // Convert string to binary blob for proper binary WebSocket frame
993 + // This ensures it's sent as opcode=2 (BINARY) not opcode=1 (TEXT)
994 + const binaryData = new Blob([largeMessage]);
995 + socket.send(binaryData);
996 + addMessageToLog(`Sent data as Blob in BINARY mode to avoid UTF-8 validation issues`, 'system');
997 + } else {
998 + socket.send(largeMessage); // Default TEXT mode
999 + addMessageToLog(`Sent data in TEXT mode (must be valid UTF-8)`, 'system');
1000 + }
1001 +
1002 + // Record send completion time
1003 + const endTime = performance.now();
1004 + const sendDuration = (endTime - startTime).toFixed(2);
1005 +
1006 + // Show first part of message with timing info
1007 + addMessageToLog(
1008 + `${largeMessage.substring(0, 50)}... [message truncated, full length: ${largeMessage.length} bytes, send time: ${sendDuration}ms]`,
1009 + 'sent'
1010 + );
1011 +
1012 + // Set up tracking for response time
1013 + const responseBenchmark = {
1014 + messageSizeKB: actualSizeKB,
1015 + sendTime: startTime,
1016 + lastResponseTime: null,
1017 + receivedBytes: 0,
1018 + expectedBytes: largeMessage.length,
1019 + originalMessage: largeMessage, // Store original message for verification
1020 + responseComplete: false
1021 + };
1022 +
1023 + // Store the benchmark data for the message listener to use
1024 + window.currentBenchmark = responseBenchmark;
1025 +
1026 + // Log whether this is sent as text or binary for verification purposes
1027 + if (sendAsBinary) {
1028 + addMessageToLog(`Verification info: Message sent as BINARY (Blob) format, will convert back for comparison`, 'system');
1029 + } else {
1030 + addMessageToLog(`Verification info: Message sent as TEXT format, expecting TEXT response`, 'system');
1031 + }
1032 + }
1033 + });
1034 +
1035 + // Track if we're currently in a benchmark test
1036 + window.currentBenchmark = null;
1037 +
1038 + // Stress test state variables
1039 + let stressTestRunning = false;
1040 + let stressTestStartTime = null;
1041 + let stressTestEndTime = null;
1042 + let stressTestInterval = null;
1043 + let stressTestTimer = null;
1044 + let pendingMessages = new Map(); // Map of messageId -> message data
1045 + const MAX_PENDING_MESSAGES = 500; // Limit concurrent pending messages to prevent browser overload
1046 +
1047 + // Stats tracking
1048 + let stressTestStats = {
1049 + messagesSent: 0,
1050 + messagesReceived: 0,
1051 + errorCount: 0,
1052 + totalLatency: 0,
1053 + bytesSent: 0,
1054 + bytesReceived: 0,
1055 + lastUpdateTime: 0,
1056 + // Per-second stats
1057 + messagesPerSecond: 0,
1058 + bytesPerSecond: 0,
1059 + latencyPerSecond: 0
1060 + };
1061 +
1062 + // Function to generate random string of specified length with variable compression efficiency
1063 + function generateRandomString(length) {
1064 + // Message identifier prefix
1065 + const prefix = `MSG-${Date.now()}-${Math.random().toString(36).substring(2, 8)}-`;
1066 + let result = prefix;
1067 +
1068 + // Calculate remaining length
1069 + const remainingLength = length - prefix.length;
1070 +
1071 + // Determine the compression pattern type for this message
1072 + // This will create a mix of highly compressible, moderately compressible, and nearly incompressible messages
1073 + const patternType = Math.floor(Math.random() * 5);
1074 +
1075 + switch(patternType) {
1076 + case 0:
1077 + // Highly compressible - repeating pattern (simulates repetitive data)
1078 + const pattern1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1079 + const repeatedPattern = pattern1.repeat(Math.ceil(remainingLength / pattern1.length));
1080 + result += repeatedPattern.substring(0, remainingLength);
1081 + break;
1082 +
1083 + case 1:
1084 + // Moderately compressible - English-like text with common words
1085 + // This simulates natural language which has moderate compression
1086 + const words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog",
1087 + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing",
1088 + "elit", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore",
1089 + "et", "dolore", "magna", "aliqua"];
1090 + let text = "";
1091 + while (text.length < remainingLength) {
1092 + text += words[Math.floor(Math.random() * words.length)] + " ";
1093 + }
1094 + result += text.substring(0, remainingLength);
1095 + break;
1096 +
1097 + case 2:
1098 + // JSON-like data with repeated keys but varying values
1099 + // Simulates structured data which compresses moderately well
1100 + let jsonData = "";
1101 + const keys = ["id", "name", "value", "timestamp", "status"];
1102 + while (jsonData.length < remainingLength) {
1103 + jsonData += `{"${keys[Math.floor(Math.random() * keys.length)]}":"${Math.random().toString(36).substring(2, 8)}",`;
1104 + jsonData += `"${keys[Math.floor(Math.random() * keys.length)]}":${Math.floor(Math.random() * 1000)},`;
1105 + jsonData += `"${keys[Math.floor(Math.random() * keys.length)]}":"${Math.random() > 0.5 ? "true" : "false"}"},`;
1106 + }
1107 + result += jsonData.substring(0, remainingLength);
1108 + break;
1109 +
1110 + case 3:
1111 + // Nearly incompressible - random data
1112 + // This simulates already compressed or encrypted data
1113 + let randomData = "";
1114 + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1115 + for (let i = 0; i < remainingLength; i++) {
1116 + randomData += chars.charAt(Math.floor(Math.random() * chars.length));
1117 + }
1118 + result += randomData;
1119 + break;
1120 +
1121 + case 4:
1122 + // Binary-like data with some patterns
1123 + // This simulates mixed binary data which has varying compression efficiency
1124 + let binaryData = "";
1125 + for (let i = 0; i < remainingLength; i++) {
1126 + // Mix of patterns and random data
1127 + if (i % 16 < 8) {
1128 + // Pattern part
1129 + binaryData += String.fromCharCode((i % 256));
1130 + } else {
1131 + // Random part
1132 + binaryData += String.fromCharCode(Math.floor(Math.random() * 256));
1133 + }
1134 + }
1135 + result += binaryData;
1136 + break;
1137 + }
1138 +
1139 + return result;
1140 + }
1141 +
1142 + // Start stress test
1143 + function startStressTest() {
1144 + if (!socket || socket.readyState !== WebSocket.OPEN) {
1145 + addStressTestLog('ERROR: WebSocket not connected', 'error');
1146 + return;
1147 + }
1148 +
1149 + // Get test parameters
1150 + const duration = parseFloat(testDurationMinutes.value);
1151 + const minSize = parseInt(minMessageSize.value);
1152 + const maxSize = parseInt(maxMessageSize.value);
1153 + const frequency = parseInt(messageFrequency.value);
1154 +
1155 + // Validate parameters
1156 + if (isNaN(duration) || duration <= 0) {
1157 + alert('Invalid test duration. Please enter a positive number.');
1158 + return;
1159 + }
1160 + if (isNaN(minSize) || minSize <= 0) {
1161 + alert('Invalid minimum message size. Please enter a positive number.');
1162 + return;
1163 + }
1164 + if (isNaN(maxSize) || maxSize <= 0 || maxSize < minSize) {
1165 + alert('Invalid maximum message size. Please enter a number greater than the minimum size.');
1166 + return;
1167 + }
1168 + if (isNaN(frequency) || frequency <= 0) {
1169 + alert('Invalid message frequency. Please enter a positive number.');
1170 + return;
1171 + }
1172 +
1173 + // Calculate test end time
1174 + stressTestStartTime = Date.now();
1175 + stressTestEndTime = stressTestStartTime + (duration * 60 * 1000);
1176 +
1177 + // Reset statistics
1178 + stressTestStats = {
1179 + messagesSent: 0,
1180 + messagesReceived: 0,
1181 + errorCount: 0,
1182 + totalLatency: 0,
1183 + bytesSent: 0,
1184 + bytesReceived: 0,
1185 + lastUpdateTime: Date.now(),
1186 + messagesPerSecond: 0,
1187 + bytesPerSecond: 0,
1188 + latencyPerSecond: 0
1189 + };
1190 +
1191 + // Clear pending messages map
1192 + pendingMessages.clear();
1193 +
1194 + // Reset UI
1195 + messagesSentElement.textContent = '0';
1196 + messagesReceivedElement.textContent = '0';
1197 + errorCountElement.textContent = '0';
1198 + avgLatencyElement.textContent = '0 ms';
1199 + bytesSentElement.textContent = '0 KB';
1200 + bytesReceivedElement.textContent = '0 KB';
1201 + errorCard.classList.remove('error');
1202 + stressTestProgress.style.width = '0%';
1203 +
1204 + // Clear log
1205 + stressTestLog.innerHTML = '';
1206 +
1207 + // Log test start
1208 + addStressTestLog(`Starting stress test with params: duration=${duration}min, size=${minSize}-${maxSize} bytes, freq=${frequency} msg/s`);
1209 +
1210 + // Update UI
1211 + startStressTestButton.disabled = true;
1212 + stopStressTestButton.disabled = false;
1213 + testDurationMinutes.disabled = true;
1214 + minMessageSize.disabled = true;
1215 + maxMessageSize.disabled = true;
1216 + useSingleSize.disabled = true;
1217 + singleMessageSize.disabled = true;
1218 + messageFrequency.disabled = true;
1219 + stressTestRunning = true;
1220 +
1221 + // Set up intervals for sending messages and updating UI
1222 + // Simple approach with no batching - one message per interval at all frequencies
1223 + const sendInterval = Math.floor(1000 / frequency);
1224 +
1225 + addStressTestLog(`Using standard timing mode: 1 message every ${sendInterval}ms`, 'info');
1226 +
1227 + // Single message per interval for all frequencies
1228 + stressTestInterval = setInterval(() => {
1229 + // Only send if we don't have too many pending messages
1230 + if (pendingMessages.size < MAX_PENDING_MESSAGES) {
1231 + sendStressTestMessage(minSize, maxSize);
1232 + } else {
1233 + addStressTestLog(`Auto-throttling: ${pendingMessages.size} pending messages`, 'warning');
1234 + }
1235 + }, sendInterval);
1236 +
1237 + // Update timer every second
1238 + stressTestTimer = setInterval(updateStressTestTimer, 1000);
1239 +
1240 + // Update initial time display
1241 + updateStressTestTimer();
1242 + }
1243 +
1244 + // Stop stress test
1245 + function stopStressTest(showSummary = true) {
1246 + if (!stressTestRunning) return;
1247 +
1248 + // Clear intervals
1249 + clearInterval(stressTestInterval);
1250 + clearInterval(stressTestTimer);
1251 +
1252 + // Update UI
1253 + startStressTestButton.disabled = false;
1254 + stopStressTestButton.disabled = true;
1255 + testDurationMinutes.disabled = false;
1256 + minMessageSize.disabled = false;
1257 + maxMessageSize.disabled = false;
1258 + useSingleSize.disabled = false;
1259 + singleMessageSize.disabled = false;
1260 + messageFrequency.disabled = false;
1261 + timeRemaining.textContent = 'Test stopped';
1262 + stressTestRunning = false;
1263 +
1264 + // Show summary if requested
1265 + if (showSummary) {
1266 + const testDuration = Math.floor((Date.now() - stressTestStartTime) / 1000);
1267 + const avgLatency = stressTestStats.messagesReceived > 0 ?
1268 + Math.round(stressTestStats.totalLatency / stressTestStats.messagesReceived) : 0;
1269 + const totalBytesSent = (stressTestStats.bytesSent / 1024).toFixed(2);
1270 + const totalBytesReceived = (stressTestStats.bytesReceived / 1024).toFixed(2);
1271 + const throughputSent = testDuration > 0 ?
1272 + (stressTestStats.bytesSent / testDuration / 1024).toFixed(2) : 0;
1273 + const throughputReceived = testDuration > 0 ?
1274 + (stressTestStats.bytesReceived / testDuration / 1024).toFixed(2) : 0;
1275 +
1276 + // Check for errors - messages that weren't properly echoed back
1277 + const unacknowledgedMessages = pendingMessages.size;
1278 + if (unacknowledgedMessages > 0) {
1279 + stressTestStats.errorCount += unacknowledgedMessages;
1280 + errorCountElement.textContent = stressTestStats.errorCount;
1281 + errorCard.classList.add('error');
1282 + addStressTestLog(`WARNING: ${unacknowledgedMessages} messages were not acknowledged`, 'error');
1283 + }
1284 +
1285 + // Add summary to log
1286 + addStressTestLog('------------------------');
1287 + addStressTestLog('TEST SUMMARY:');
1288 + addStressTestLog(`Test duration: ${testDuration} seconds`);
1289 + addStressTestLog(`Messages sent: ${stressTestStats.messagesSent}`);
1290 + addStressTestLog(`Messages received: ${stressTestStats.messagesReceived}`);
1291 + addStressTestLog(`Errors: ${stressTestStats.errorCount}`);
1292 + addStressTestLog(`Average latency: ${avgLatency} ms`);
1293 + addStressTestLog(`Data sent: ${totalBytesSent} KB (${throughputSent} KB/s)`);
1294 + addStressTestLog(`Data received: ${totalBytesReceived} KB (${throughputReceived} KB/s)`);
1295 +
1296 + // Check success rate
1297 + const successRate = stressTestStats.messagesSent > 0 ?
1298 + Math.round((stressTestStats.messagesReceived / stressTestStats.messagesSent) * 100) : 0;
1299 + addStressTestLog(`Success rate: ${successRate}%`);
1300 +
1301 + if (successRate < 100) {
1302 + addStressTestLog('Some messages were not correctly echoed back!', 'error');
1303 + } else if (stressTestStats.errorCount === 0) {
1304 + addStressTestLog('Test completed successfully with no errors!', 'success');
1305 + }
1306 + }
1307 +
1308 + // Clear any pending messages
1309 + pendingMessages.clear();
1310 + }
1311 +
1312 + // Send a stress test message
1313 + function sendStressTestMessage(minSize, maxSize) {
1314 + if (!stressTestRunning || !socket || socket.readyState !== WebSocket.OPEN) {
1315 + return;
1316 + }
1317 +
1318 + // Check if test should end
1319 + if (Date.now() >= stressTestEndTime) {
1320 + stopStressTest();
1321 + return;
1322 + }
1323 +
1324 + // Check if we need to throttle due to too many pending messages
1325 + if (pendingMessages.size >= MAX_PENDING_MESSAGES) {
1326 + addStressTestLog(`Throttling: ${pendingMessages.size} pending messages`, 'warning');
1327 + return;
1328 + }
1329 +
1330 + // Generate message size based on user settings
1331 + let messageSize;
1332 +
1333 + if (useSingleSize.checked) {
1334 + // Use fixed size if that option is selected
1335 + messageSize = parseInt(singleMessageSize.value);
1336 + if (isNaN(messageSize) || messageSize < 100 || messageSize > 100000) {
1337 + messageSize = 5000; // Default if invalid
1338 + }
1339 + } else {
1340 + // Generate random size within range, but cap at 32KB to reduce fragmentation issues
1341 + const safeMaxSize = Math.min(maxSize, 32768);
1342 + messageSize = Math.floor(Math.random() * (safeMaxSize - minSize + 1)) + minSize;
1343 + }
1344 +
1345 + // Generate random message with unique ID embedded at the start
1346 + const message = generateRandomString(messageSize);
1347 + const messageId = message.substring(0, message.indexOf('-', 4) + 1); // Extract the unique prefix
1348 +
1349 + try {
1350 + // Record send time and track message
1351 + const sendTime = Date.now();
1352 + pendingMessages.set(messageId, {
1353 + id: messageId,
1354 + sentTime: sendTime,
1355 + size: message.length,
1356 + received: false
1357 + });
1358 +
1359 + // Log large messages that may trigger fragmentation
1360 + if (message.length > 10000) {
1361 + addStressTestLog(`Sending large message (${message.length} bytes) - may be fragmented by browser`, 'warning');
1362 + }
1363 +
1364 + // Debug logs for detailed tracking
1365 + addStressTestLog(`Sending message ${messageId} (${message.length} bytes)`, 'debug');
1366 +
1367 + // Send the message
1368 + socket.send(message);
1369 +
1370 + // Update stats
1371 + stressTestStats.messagesSent++;
1372 + stressTestStats.bytesSent += message.length;
1373 + messagesSentElement.textContent = stressTestStats.messagesSent;
1374 + bytesSentElement.textContent = (stressTestStats.bytesSent / 1024).toFixed(2) + ' KB';
1375 + } catch (error) {
1376 + console.error('Error sending message:', error);
1377 + stressTestStats.errorCount++;
1378 + errorCountElement.textContent = stressTestStats.errorCount;
1379 + errorCard.classList.add('error');
1380 +
1381 + addStressTestLog(`ERROR sending message: ${error.message}`, 'error');
1382 + }
1383 + }
1384 +
1385 + // Update stress test timer and progress
1386 + function updateStressTestTimer() {
1387 + if (!stressTestRunning) return;
1388 +
1389 + const now = Date.now();
1390 + const elapsed = now - stressTestStartTime;
1391 + const total = stressTestEndTime - stressTestStartTime;
1392 + const remaining = Math.max(0, stressTestEndTime - now);
1393 +
1394 + // Update progress bar
1395 + const progressPercent = Math.min(100, (elapsed / total) * 100);
1396 + stressTestProgress.style.width = `${progressPercent}%`;
1397 +
1398 + // Update time remaining
1399 + const minutes = Math.floor(remaining / 60000);
1400 + const seconds = Math.floor((remaining % 60000) / 1000);
1401 + timeRemaining.textContent = `Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')}`;
1402 +
1403 + // Update real-time stats (every second)
1404 + const secondsSinceLastUpdate = (now - stressTestStats.lastUpdateTime) / 1000;
1405 + if (secondsSinceLastUpdate >= 1) {
1406 + // Calculate per-second rates
1407 + stressTestStats.messagesPerSecond = Math.round(
1408 + (stressTestStats.messagesSent - stressTestStats.messagesPerSecond) / secondsSinceLastUpdate
1409 + );
1410 + stressTestStats.bytesPerSecond = Math.round(
1411 + (stressTestStats.bytesSent - stressTestStats.bytesPerSecond) / secondsSinceLastUpdate
1412 + );
1413 +
1414 + // Update last update time
1415 + stressTestStats.lastUpdateTime = now;
1416 + }
1417 +
1418 + // Check for test completion
1419 + if (remaining <= 0) {
1420 + stopStressTest();
1421 + }
1422 + }
1423 +
1424 + // Add a log entry to the stress test log
1425 + function addStressTestLog(message, type = 'info', forceLog = false) {
1426 + // Skip debug messages unless debug mode is enabled or forceLog is true
1427 + if (type === 'debug' && !enableDebugMode.checked && !forceLog) {
1428 + return;
1429 + }
1430 +
1431 + const logEntry = document.createElement('div');
1432 + logEntry.className = `log-entry ${type}`;
1433 +
1434 + // Add timestamp
1435 + const timestamp = new Date().toLocaleTimeString();
1436 +
1437 + // Format log entry
1438 + logEntry.textContent = `[${timestamp}] ${message}`;
1439 +
1440 + // Add styles based on type
1441 + if (type === 'error') {
1442 + logEntry.style.color = '#dc3545';
1443 + } else if (type === 'success') {
1444 + logEntry.style.color = '#28a745';
1445 + } else if (type === 'warning') {
1446 + logEntry.style.color = '#ffc107';
1447 + } else if (type === 'debug') {
1448 + logEntry.style.color = '#6c757d';
1449 + logEntry.style.fontSize = '0.9em';
1450 + }
1451 +
1452 + // Add to log and scroll to bottom
1453 + stressTestLog.appendChild(logEntry);
1454 + stressTestLog.scrollTop = stressTestLog.scrollHeight;
1455 + }
1456 +
1457 + // Verify and process stress test response
1458 + function processStressTestResponse(message) {
1459 + // Try to extract the message ID from the response
1460 + const messageId = message.substring(0, message.indexOf('-', 4) + 1);
1461 +
1462 + // Debug log for message receipt
1463 + addStressTestLog(`Received message with ID ${messageId} (${message.length} bytes)`, 'debug');
1464 +
1465 + // Check if this is a response to a tracked message
1466 + if (pendingMessages.has(messageId)) {
1467 + const pendingMessage = pendingMessages.get(messageId);
1468 + const receiveTime = Date.now();
1469 + const latency = receiveTime - pendingMessage.sentTime;
1470 +
1471 + // Verify the message content
1472 + if (message.length < 20) {
1473 + // Message is severely truncated/corrupted
1474 + stressTestStats.errorCount++;
1475 + errorCountElement.textContent = stressTestStats.errorCount;
1476 + errorCard.classList.add('error');
1477 + addStressTestLog(`ERROR: Received severely truncated message: ${messageId}, length=${message.length}`, 'error');
1478 + }
1479 + else if (message !== pendingMessage.id && message.indexOf('-', 4) !== pendingMessage.id.indexOf('-', 4)) {
1480 + // The message ID structure appears to be corrupted - this could indicate decompression issues
1481 + addStressTestLog(`WARNING: Message ID structure changed: original=${pendingMessage.id}, received=${messageId}`, 'warning');
1482 + }
1483 +
1484 + // Update stats
1485 + stressTestStats.messagesReceived++;
1486 + stressTestStats.bytesReceived += message.length;
1487 + stressTestStats.totalLatency += latency;
1488 +
1489 + // Record original message size for statistics (no discrepancy logging)
1490 + const originalSize = pendingMessage.size;
1491 +
1492 + // Update UI
1493 + messagesReceivedElement.textContent = stressTestStats.messagesReceived;
1494 + bytesReceivedElement.textContent = (stressTestStats.bytesReceived / 1024).toFixed(2) + ' KB';
1495 + const avgLatency = Math.round(stressTestStats.totalLatency / stressTestStats.messagesReceived);
1496 + avgLatencyElement.textContent = avgLatency + ' ms';
1497 +
1498 + // Every 50 messages, log current stats to help with debugging
1499 + if (stressTestStats.messagesReceived % 50 === 0) {
1500 + addStressTestLog(`Progress: ${stressTestStats.messagesReceived}/${stressTestStats.messagesSent} messages, avg latency: ${avgLatency}ms`);
1501 + }
1502 +
1503 + // Remove from pending messages
1504 + pendingMessages.delete(messageId);
1505 +
1506 + return true;
1507 + }
1508 +
1509 + return false;
1510 + }
1511 +
1512 + function sendMessage() {
1513 + const message = messageInput.value.trim();
1514 + if (message && socket && socket.readyState === WebSocket.OPEN) {
1515 + socket.send(message);
1516 + addMessageToLog(message, 'sent');
1517 + messageInput.value = '';
1518 + }
1519 + }
1520 +
1521 + // Add message to the log
1522 + function addMessageToLog(message, type, isHtml = false, additionalInfo = '') {
1523 + const messageElement = document.createElement('div');
1524 + messageElement.className = `message ${type}`;
1525 +
1526 + // Handle different message types
1527 + if (type === 'system') {
1528 + // System messages can contain HTML if specified
1529 + if (isHtml) {
1530 + messageElement.innerHTML = message;
1531 + } else {
1532 + messageElement.textContent = message;
1533 + }
1534 + messageElement.style.backgroundColor = '#fff3cd';
1535 + messageElement.style.color = '#856404';
1536 + messageElement.style.fontStyle = 'italic';
1537 + }
1538 + else if (type === 'received' || type === 'sent') {
1539 + // For regular messages, check size
1540 + if (message.length > 1000) {
1541 + // For large messages, create a collapsible view
1542 + const messageSizeKB = (message.length / 1024).toFixed(2);
1543 +
1544 + // Create summary with expand button
1545 + const summary = document.createElement('div');
1546 + summary.innerHTML = `
1547 + <span class="message-preview">${message.substring(0, 500)}...</span>
1548 + <div class="message-info">
1549 + Message length: ${message.length} bytes (${messageSizeKB} KB)${additionalInfo}
1550 + <button class="toggle-button">Show Full Message</button>
1551 + </div>
1552 + `;
1553 +
1554 + // Create content div (initially hidden)
1555 + const content = document.createElement('div');
1556 + content.className = 'full-message';
1557 + content.style.display = 'none';
1558 + content.style.maxHeight = '300px';
1559 + content.style.overflow = 'auto';
1560 + content.style.border = '1px solid #ddd';
1561 + content.style.marginTop = '5px';
1562 + content.style.padding = '5px';
1563 + content.textContent = message;
1564 +
1565 + // Add toggle functionality
1566 + const toggleButton = summary.querySelector('.toggle-button');
1567 + toggleButton.style.marginLeft = '10px';
1568 + toggleButton.style.padding = '2px 5px';
1569 + toggleButton.style.fontSize = '0.8em';
1570 + toggleButton.addEventListener('click', function() {
1571 + if (content.style.display === 'none') {
1572 + content.style.display = 'block';
1573 + this.textContent = 'Hide Full Message';
1574 + } else {
1575 + content.style.display = 'none';
1576 + this.textContent = 'Show Full Message';
1577 + }
1578 + });
1579 +
1580 + // Add elements to message container
1581 + messageElement.appendChild(summary);
1582 + messageElement.appendChild(content);
1583 + } else {
1584 + // Normal sized message, show in full with any additional info
1585 + if (additionalInfo) {
1586 + const wrapper = document.createElement('div');
1587 +
1588 + // Add the message text
1589 + const messageText = document.createElement('div');
1590 + messageText.textContent = message;
1591 + wrapper.appendChild(messageText);
1592 +
1593 + // Add additional info
1594 + const infoText = document.createElement('div');
1595 + infoText.style.fontSize = '0.85em';
1596 + infoText.style.color = '#666';
1597 + infoText.style.marginTop = '3px';
1598 + infoText.textContent = additionalInfo.trim();
1599 + wrapper.appendChild(infoText);
1600 +
1601 + messageElement.appendChild(wrapper);
1602 + } else {
1603 + messageElement.textContent = message;
1604 + }
1605 + }
1606 + }
1607 +
1608 + // Add timestamp
1609 + const timestamp = new Date().toLocaleTimeString();
1610 + const timeElement = document.createElement('span');
1611 + timeElement.className = 'timestamp';
1612 + timeElement.textContent = timestamp;
1613 + timeElement.style.fontSize = '0.8em';
1614 + timeElement.style.color = '#666';
1615 + timeElement.style.marginRight = '5px';
1616 + timeElement.style.fontWeight = 'bold';
1617 +
1618 + // For HTML content, we need to insert differently
1619 + if (type === 'system' && isHtml) {
1620 + // Create a container for the timestamp
1621 + const timestampContainer = document.createElement('div');
1622 + timestampContainer.appendChild(timeElement);
1623 +
1624 + // Prepend the timestamp container
1625 + messageElement.prepend(timestampContainer);
1626 + } else {
1627 + messageElement.prepend(timeElement);
1628 + }
1629 +
1630 + // Add to log and scroll to bottom
1631 + messagesLog.appendChild(messageElement);
1632 + messagesLog.scrollTop = messagesLog.scrollHeight;
1633 + }
1634 +
1635 + // Toggle between ws:// and wss:// protocols
1636 + toggleProtocolButton.addEventListener('click', () => {
1637 + const currentUrl = endpointInput.value.trim();
1638 +
1639 + if (currentUrl.startsWith('ws://')) {
1640 + // Switch from ws:// to wss://
1641 + endpointInput.value = currentUrl.replace('ws://', 'wss://');
1642 + toggleProtocolButton.textContent = 'Switch to ws://';
1643 + } else if (currentUrl.startsWith('wss://')) {
1644 + // Switch from wss:// to ws://
1645 + endpointInput.value = currentUrl.replace('wss://', 'ws://');
1646 + toggleProtocolButton.textContent = 'Switch to wss://';
1647 + } else {
1648 + // Invalid URL, add ws:// prefix
1649 + endpointInput.value = 'ws://' + currentUrl;
1650 + toggleProtocolButton.textContent = 'Switch to wss://';
1651 + }
1652 + });
1653 +
1654 + // Stress test button handlers
1655 + startStressTestButton.addEventListener('click', startStressTest);
1656 + stopStressTestButton.addEventListener('click', () => stopStressTest(true));
1657 + </script>
1658 +</body>
1659 +</html>
\ No newline at end of file
src/web/websocket/websocket-echo.c new
+56
@@ -0,0 +1,56 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-echo.h"
4 +
5 +// Called when a client is connected and ready to exchange messages
6 +void echo_on_connect(struct websocket_server_client *wsc) {
7 + if (!wsc) return;
8 +
9 + websocket_debug(wsc, "Echo protocol client connected");
10 +
11 + // Send a welcome message
12 + // websocket_protocol_send_text(wsc, "Welcome to Netdata Echo WebSocket Server");
13 +}
14 +
15 +// Called when a message is received from the client
16 +void echo_on_message_callback(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode) {
17 + if (!wsc || !message)
18 + return;
19 +
20 + websocket_debug(wsc, "Echo protocol handling message: type=%s, length=%zu",
21 + (opcode == WS_OPCODE_BINARY) ? "binary" : "text",
22 + length);
23 +
24 + // Simply echo back the same message with the same opcode
25 + websocket_protocol_send_frame(wsc, message, length, opcode, true);
26 +}
27 +
28 +// Called before sending a close frame to the client
29 +void echo_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason) {
30 + if (!wsc) return;
31 +
32 + websocket_debug(wsc, "Echo protocol client closing with code %d (%s): %s",
33 + code,
34 + code == WS_CLOSE_NORMAL ? "Normal" :
35 + code == WS_CLOSE_GOING_AWAY ? "Going Away" :
36 + code == WS_CLOSE_PROTOCOL_ERROR ? "Protocol Error" :
37 + code == WS_CLOSE_INTERNAL_ERROR ? "Internal Error" : "Other",
38 + reason ? reason : "No reason provided");
39 +
40 + // Optional: Send a goodbye message
41 + // websocket_protocol_send_text(wsc, "Goodbye from Netdata Echo WebSocket Server");
42 +}
43 +
44 +// Called when a client is about to be disconnected
45 +void echo_on_disconnect(struct websocket_server_client *wsc) {
46 + if (!wsc) return;
47 +
48 + websocket_debug(wsc, "Echo protocol client disconnected");
49 +
50 + // No cleanup needed for the Echo protocol since it doesn't maintain any state
51 +}
52 +
53 +// Initialize the Echo protocol
54 +void websocket_echo_initialize(void) {
55 + netdata_log_info("Echo protocol initialized");
56 +}
\ No newline at end of file
src/web/websocket/websocket-echo.h new
+17
@@ -0,0 +1,17 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEBSOCKET_ECHO_H
4 +#define NETDATA_WEBSOCKET_ECHO_H
5 +
6 +#include "websocket-internal.h"
7 +
8 +// WebSocket protocol handler callbacks for Echo protocol
9 +void echo_on_connect(struct websocket_server_client *wsc);
10 +void echo_on_message_callback(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode);
11 +void echo_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason);
12 +void echo_on_disconnect(struct websocket_server_client *wsc);
13 +
14 +// Initialize Echo protocol - called during WebSocket subsystem initialization
15 +void websocket_echo_initialize(void);
16 +
17 +#endif // NETDATA_WEBSOCKET_ECHO_H
\ No newline at end of file
src/web/websocket/websocket-handshake.c new
+436
@@ -0,0 +1,436 @@
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 +
9 +// Global array of WebSocket threads
10 +WEBSOCKET_THREAD websocket_threads[WEBSOCKET_MAX_THREADS];
11 +
12 +// Initialize WebSocket thread system
13 +void websocket_threads_init(void) {
14 + for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
15 + websocket_threads[i].id = i;
16 + websocket_threads[i].thread = NULL;
17 + websocket_threads[i].running = false;
18 + spinlock_init(&websocket_threads[i].spinlock);
19 + websocket_threads[i].clients_current = 0;
20 + spinlock_init(&websocket_threads[i].clients_spinlock);
21 + websocket_threads[i].clients = NULL;
22 + websocket_threads[i].ndpl = NULL;
23 + websocket_threads[i].cmd.pipe[PIPE_READ] = -1;
24 + websocket_threads[i].cmd.pipe[PIPE_WRITE] = -1;
25 + }
26 +}
27 +
28 +// Find the thread with the minimum client load and atomically increment its count
29 +NEVERNULL
30 +static WEBSOCKET_THREAD *websocket_thread_get_min_load(void) {
31 + // Static spinlock to protect the critical section of thread selection
32 + static SPINLOCK assign_spinlock = SPINLOCK_INITIALIZER;
33 + size_t slot = 0;
34 +
35 + // Critical section: find thread with minimum load and increment its count atomically
36 + spinlock_lock(&assign_spinlock);
37 +
38 + // Find the minimum load thread
39 + size_t min_clients = websocket_threads[0].clients_current;
40 +
41 + for(size_t i = 1; i < WEBSOCKET_MAX_THREADS; i++) {
42 + // Check if this thread has fewer clients
43 + if(websocket_threads[i].clients_current < min_clients) {
44 + min_clients = websocket_threads[i].clients_current;
45 + slot = i;
46 + }
47 + }
48 +
49 + // Preemptively increment the client count to prevent race conditions
50 + // This ensures concurrent client assignments will be properly distributed
51 + websocket_threads[slot].clients_current++;
52 +
53 + spinlock_unlock(&assign_spinlock);
54 +
55 + return &websocket_threads[slot];
56 +}
57 +
58 +// Handle socket takeover from web client - similar to stream_receiver_takeover_web_connection
59 +static void websocket_takeover_web_connection(struct web_client *w, WS_CLIENT *wsc) {
60 + // Set the file descriptor and ssl from the web client
61 + wsc->sock.fd = w->fd;
62 + wsc->sock.ssl = w->ssl;
63 +
64 + w->ssl = NETDATA_SSL_UNSET_CONNECTION;
65 +
66 + WEB_CLIENT_IS_DEAD(w);
67 +
68 + if(web_server_mode == WEB_SERVER_MODE_STATIC_THREADED) {
69 + web_client_flag_set(w, WEB_CLIENT_FLAG_DONT_CLOSE_SOCKET);
70 + }
71 + else {
72 + w->fd = -1;
73 + }
74 +
75 + // Clear web client buffer
76 + buffer_flush(w->response.data);
77 +
78 + web_server_remove_current_socket_from_poll();
79 +}
80 +
81 +// Initialize a thread's poll
82 +static bool websocket_thread_init_poll(WEBSOCKET_THREAD *wth) {
83 + // Create poll instance
84 + if(!wth->ndpl) {
85 + wth->ndpl = nd_poll_create();
86 + if (!wth->ndpl) {
87 + netdata_log_error("WEBSOCKET[%zu]: Failed to create poll", wth->id);
88 + goto cleanup;
89 + }
90 + }
91 +
92 + // Create command pipe
93 + if(wth->cmd.pipe[PIPE_READ] == -1 || wth->cmd.pipe[PIPE_WRITE] == -1) {
94 + if (pipe(wth->cmd.pipe) == -1) {
95 + netdata_log_error("WEBSOCKET[%zu]: Failed to create command pipe: %s", wth->id, strerror(errno));
96 + goto cleanup;
97 + }
98 +
99 + // Set pipe to non-blocking
100 + if(fcntl(wth->cmd.pipe[PIPE_READ], F_SETFL, O_NONBLOCK) == -1) {
101 + netdata_log_error("WEBSOCKET[%zu]: Failed to set command pipe to non-blocking: %s", wth->id, strerror(errno));
102 + goto cleanup;
103 + }
104 +
105 + // Add command pipe to poll
106 + bool added = nd_poll_add(wth->ndpl, wth->cmd.pipe[PIPE_READ], ND_POLL_READ, &wth->cmd);
107 + if(!added) {
108 + netdata_log_error("WEBSOCKET[%zu]: Failed to add command pipe to poll", wth->id);
109 + goto cleanup;
110 + }
111 + }
112 +
113 + return true;
114 +
115 +cleanup:
116 + if(wth->cmd.pipe[PIPE_READ] != -1) {
117 + close(wth->cmd.pipe[PIPE_READ]);
118 + wth->cmd.pipe[PIPE_READ] = -1;
119 + }
120 + if(wth->cmd.pipe[PIPE_WRITE] != -1) {
121 + close(wth->cmd.pipe[PIPE_WRITE]);
122 + wth->cmd.pipe[PIPE_WRITE] = -1;
123 + }
124 + if(wth->ndpl) {
125 + nd_poll_destroy(wth->ndpl);
126 + wth->ndpl = NULL;
127 + }
128 + return false;
129 +}
130 +
131 +// Assign a client to a thread
132 +static WEBSOCKET_THREAD *websocket_thread_assign_client(WS_CLIENT *wsc) {
133 + // Get the thread with the minimum load
134 + // Note: client count is already atomically incremented inside this function
135 + WEBSOCKET_THREAD *wth = websocket_thread_get_min_load();
136 +
137 + // Lock the thread for initialization
138 + spinlock_lock(&wth->spinlock);
139 +
140 + // Start the thread if not running
141 + if(!wth->thread) {
142 + // Initialize poll
143 + if(!websocket_thread_init_poll(wth)) {
144 + spinlock_unlock(&wth->spinlock);
145 + netdata_log_error("WEBSOCKET[%zu]: Failed to initialize poll", wth->id);
146 + goto undo;
147 + }
148 +
149 + char thread_name[32];
150 + snprintf(thread_name, sizeof(thread_name), "WEBSOCK[%zu]", wth->id);
151 + wth->thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT, websocket_thread, wth);
152 + wth->running = true;
153 + }
154 +
155 + // Release the thread lock
156 + spinlock_unlock(&wth->spinlock);
157 +
158 + // Link thread to client
159 + wsc->wth = wth;
160 +
161 + // Send command to add client
162 + if(!websocket_thread_send_command(wth, WEBSOCKET_THREAD_CMD_ADD_CLIENT, wsc->id)) {
163 + netdata_log_error("WEBSOCKET[%zu]: Failed to send add client command", wth->id);
164 + goto undo;
165 + }
166 +
167 + return wth;
168 +
169 +undo:
170 + // Roll back the client count increment since assignment failed
171 + wsc->wth = NULL;
172 +
173 + if(wth) {
174 + spinlock_lock(&wth->clients_spinlock);
175 + if (wth->clients_current > 0)
176 + wth->clients_current--;
177 + spinlock_unlock(&wth->clients_spinlock);
178 + }
179 +
180 + return NULL;
181 +}
182 +
183 +// Cancel all WebSocket threads
184 +void websocket_threads_join(void) {
185 + for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
186 + if(websocket_threads[i].thread) {
187 + // Send exit command
188 + websocket_thread_send_command(&websocket_threads[i], WEBSOCKET_THREAD_CMD_EXIT, 0);
189 +
190 + // Signal thread to cancel
191 + nd_thread_signal_cancel(websocket_threads[i].thread);
192 + }
193 + }
194 +
195 + // Wait for all threads to exit
196 + for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
197 + if(websocket_threads[i].thread) {
198 + nd_thread_join(websocket_threads[i].thread);
199 + websocket_threads[i].thread = NULL;
200 + websocket_threads[i].running = false;
201 + }
202 + }
203 +}
204 +
205 +// Check if the current HTTP request is a WebSocket handshake request
206 +static bool websocket_detect_handshake_request(struct web_client *w) {
207 + // We need a valid key and to be flagged as a WebSocket request
208 + if (!web_client_is_websocket(w) || !w->websocket.key)
209 + return false;
210 +
211 + return true;
212 +}
213 +
214 +// Generate the WebSocket accept key as per RFC 6455
215 +static char *websocket_generate_handshake_key(const char *client_key) {
216 + if (!client_key)
217 + return NULL;
218 +
219 + // Concatenate the key with the WebSocket GUID
220 + char concat_key[256];
221 + snprintfz(concat_key, sizeof(concat_key), "%s%s", client_key, WS_GUID);
222 +
223 + // Create SHA-1 hash
224 + unsigned char sha_hash[SHA_DIGEST_LENGTH];
225 + SHA1((unsigned char *)concat_key, strlen(concat_key), sha_hash);
226 +
227 + // Convert to base64
228 + char *accept_key = mallocz(33); // Base64 of SHA-1 is 28 chars + null term
229 + netdata_base64_encode((unsigned char *)accept_key, sha_hash, SHA_DIGEST_LENGTH);
230 +
231 + return accept_key;
232 +}
233 +
234 +static bool websocket_send_first_response(WS_CLIENT *wsc, const char *accept_key, WEBSOCKET_EXTENSION ext_flags, bool url_protocol) {
235 + CLEAN_BUFFER *wb = buffer_create(1024, NULL);
236 +
237 + buffer_sprintf(wb,
238 + "HTTP/1.1 101 Switching Protocols\r\n"
239 + "Server: Netdata\r\n"
240 + "Upgrade: websocket\r\n"
241 + "Connection: Upgrade\r\n"
242 + "Sec-WebSocket-Accept: %s\r\n",
243 + accept_key
244 + );
245 +
246 + // Add the selected subprotocol
247 + if(!url_protocol && wsc->protocol != WS_PROTOCOL_UNKNOWN && wsc->protocol != WS_PROTOCOL_DEFAULT)
248 + buffer_sprintf(wb, "Sec-WebSocket-Protocol: %s\r\n", WEBSOCKET_PROTOCOL_2str(wsc->protocol));
249 +
250 + switch (wsc->compression.type) {
251 + case WS_COMPRESS_DEFLATE:
252 + buffer_strcat(wb, "Sec-WebSocket-Extensions: permessage-deflate");
253 +
254 + // Add parameters if different from defaults
255 + if (!wsc->compression.client_context_takeover)
256 + buffer_strcat(wb, "; client_no_context_takeover");
257 +
258 + if (!wsc->compression.server_context_takeover)
259 + buffer_strcat(wb, "; server_no_context_takeover");
260 +
261 + if(ext_flags & WS_EXTENSION_SERVER_MAX_WINDOW_BITS)
262 + buffer_sprintf(wb, "; server_max_window_bits=%d", wsc->compression.server_max_window_bits);
263 +
264 + if(ext_flags & WS_EXTENSION_CLIENT_MAX_WINDOW_BITS)
265 + buffer_sprintf(wb, "; client_max_window_bits=%d", wsc->compression.client_max_window_bits);
266 +
267 + buffer_strcat(wb, "\r\n");
268 + break;
269 +
270 + default:
271 + break;
272 + }
273 +
274 + // End of headers
275 + buffer_strcat(wb, "Sec-WebSocket-Version: 13\r\n");
276 + buffer_strcat(wb, "\r\n");
277 +
278 + // Send the handshake response using ND_SOCK - we're still in the web server thread,
279 + // so we need to use the persist version to ensure the complete handshake is sent
280 + const char *header_str = buffer_tostring(wb);
281 + size_t header_len = buffer_strlen(wb);
282 + ssize_t bytes = nd_sock_write_persist(&wsc->sock, header_str, header_len, 20);
283 +
284 + websocket_debug(wsc, "Sent WebSocket handshake response: %zd bytes out of %zu bytes", bytes, header_len);
285 + return bytes == (ssize_t)header_len;
286 +}
287 +
288 +// Handle the WebSocket handshake procedure
289 +short int websocket_handle_handshake(struct web_client *w) {
290 + if (!websocket_detect_handshake_request(w))
291 + return HTTP_RESP_BAD_REQUEST;
292 +
293 + // Generate the accept key
294 + char *accept_key = websocket_generate_handshake_key(w->websocket.key);
295 + if (!accept_key)
296 + return HTTP_RESP_INTERNAL_SERVER_ERROR;
297 +
298 + // Create the WebSocket client object early so we can set up compression
299 + WS_CLIENT *wsc = websocket_client_create();
300 +
301 + // Copy client information
302 + strncpyz(wsc->client_ip, w->client_ip, sizeof(wsc->client_ip));
303 + strncpyz(wsc->client_port, w->client_port, sizeof(wsc->client_port));
304 +
305 + bool url_protocol = false;
306 + wsc->protocol = w->websocket.protocol;
307 +
308 + if(wsc->protocol == WS_PROTOCOL_DEFAULT) {
309 + const char *path = buffer_tostring(w->url_path_decoded);
310 + if (path && path[0] == '/' && path[1])
311 + wsc->protocol = WEBSOCKET_PROTOCOL_2id(&path[1]);
312 +
313 + url_protocol = true;
314 + }
315 +
316 + // If no protocol is selected by either URL or subprotocol, reject the connection
317 + if(wsc->protocol == WS_PROTOCOL_UNKNOWN || wsc->protocol == WS_PROTOCOL_DEFAULT) {
318 + netdata_log_error("WEBSOCKET: No valid protocol selected by either URL or subprotocol");
319 + freez(accept_key);
320 + websocket_client_free(wsc);
321 + return HTTP_RESP_BAD_REQUEST;
322 + }
323 +
324 + // Take over the connection immediately
325 + websocket_takeover_web_connection(w, wsc);
326 +
327 + if((w->websocket.ext_flags & WS_EXTENSION_PERMESSAGE_DEFLATE)) {
328 + wsc->compression.enabled = true;
329 + wsc->compression.type = WS_COMPRESS_DEFLATE;
330 +
331 + if (w->websocket.ext_flags & WS_EXTENSION_CLIENT_NO_CONTEXT_TAKEOVER)
332 + wsc->compression.client_context_takeover = false;
333 + else
334 + wsc->compression.client_context_takeover = true;
335 +
336 + if (w->websocket.ext_flags & WS_EXTENSION_SERVER_NO_CONTEXT_TAKEOVER)
337 + wsc->compression.server_context_takeover = false;
338 + else
339 + wsc->compression.server_context_takeover = true;
340 +
341 + // Set window bits for both client-to-server and server-to-client directions
342 + wsc->compression.client_max_window_bits = w->websocket.client_max_window_bits ? w->websocket.client_max_window_bits : WS_COMPRESS_WINDOW_BITS;
343 + wsc->compression.server_max_window_bits = w->websocket.server_max_window_bits ? w->websocket.server_max_window_bits : WS_COMPRESS_WINDOW_BITS;
344 + }
345 +
346 + if(!websocket_send_first_response(wsc, accept_key, w->websocket.ext_flags, url_protocol)) {
347 + netdata_log_error("WEBSOCKET: Failed to send complete WebSocket handshake response"); // No client yet
348 + freez(accept_key);
349 + websocket_client_free(wsc);
350 + return HTTP_RESP_INTERNAL_SERVER_ERROR;
351 + }
352 +
353 + freez(accept_key);
354 +
355 + // Now that we've sent the handshake response successfully, set the connection state to open
356 + wsc->state = WS_STATE_OPEN;
357 +
358 + // Set up protocol-specific callbacks based on the selected protocol
359 + switch (wsc->protocol) {
360 +#ifdef NETDATA_INTERNAL_CHECKS
361 + case WS_PROTOCOL_JSONRPC:
362 + // Set up callbacks for jsonrpc protocol
363 + wsc->on_connect = jsonrpc_on_connect;
364 + wsc->on_message = jsonrpc_on_message_callback;
365 + wsc->on_close = jsonrpc_on_close;
366 + wsc->on_disconnect = jsonrpc_on_disconnect;
367 + websocket_debug(wsc, "Setting up jsonrpc protocol callbacks");
368 + break;
369 +
370 + case WS_PROTOCOL_ECHO:
371 + // Set up callbacks for echo protocol
372 + wsc->on_connect = echo_on_connect;
373 + wsc->on_message = echo_on_message_callback;
374 + wsc->on_close = echo_on_close;
375 + wsc->on_disconnect = echo_on_disconnect;
376 + websocket_debug(wsc, "Setting up echo protocol callbacks");
377 + break;
378 +
379 + case WS_PROTOCOL_MCP:
380 + // Set up callbacks for MCP protocol
381 + wsc->on_connect = mcp_websocket_on_connect;
382 + wsc->on_message = mcp_websocket_on_message;
383 + wsc->on_close = mcp_websocket_on_close;
384 + wsc->on_disconnect = mcp_websocket_on_disconnect;
385 + websocket_debug(wsc, "Setting up MCP protocol callbacks");
386 + break;
387 +#endif
388 +
389 + default:
390 + // No protocol handler available - this shouldn't happen as we check earlier
391 + netdata_log_error("WEBSOCKET: No handler available for protocol %d", wsc->protocol);
392 + websocket_client_free(wsc);
393 + return HTTP_RESP_BAD_REQUEST;
394 + }
395 +
396 + // Register the client in our registry
397 + if (!websocket_client_register(wsc)) {
398 + websocket_error(wsc, "Failed to register WebSocket client");
399 + websocket_client_free(wsc);
400 + return HTTP_RESP_WEBSOCKET_HANDSHAKE;
401 + }
402 +
403 + // Message structures are already initialized in websocket_client_create()
404 +
405 + // Set socket to non-blocking mode
406 + if (fcntl(wsc->sock.fd, F_SETFL, O_NONBLOCK) == -1) {
407 + websocket_error(wsc, "Failed to set WebSocket socket to non-blocking mode");
408 + websocket_client_free(wsc);
409 + return HTTP_RESP_WEBSOCKET_HANDSHAKE;
410 + }
411 +
412 + // Assign to a thread
413 + WEBSOCKET_THREAD *wth = websocket_thread_assign_client(wsc);
414 + if (!wth) {
415 + websocket_error(wsc, "Failed to assign WebSocket client to a thread");
416 + websocket_client_free(wsc);
417 + return HTTP_RESP_WEBSOCKET_HANDSHAKE;
418 + }
419 +
420 + nd_log(NDLS_DAEMON, NDLP_DEBUG,
421 + "WebSocket connection established with %s:%s using protocol: %s (client ID: %u, thread: %zu), "
422 + "compression: %s (client context takeover: %s, server context takeover: %s, "
423 + "client window bits: %d, server window bits: %d)",
424 + wsc->client_ip, wsc->client_port,
425 + WEBSOCKET_PROTOCOL_2str(wsc->protocol),
426 + wsc->id, wth->id,
427 + wsc->compression.enabled ? "enabled" : "disabled",
428 + wsc->compression.client_context_takeover ? "enabled" : "disabled",
429 + wsc->compression.server_context_takeover ? "enabled" : "disabled",
430 + wsc->compression.client_max_window_bits,
431 + wsc->compression.server_max_window_bits);
432 +
433 + // Important: This code doesn't actually get sent to the client since we've already
434 + // taken over the socket. It's just used by the caller to identify what happened.
435 + return HTTP_RESP_WEBSOCKET_HANDSHAKE;
436 +}
src/web/websocket/websocket-internal.h new
+252
@@ -0,0 +1,252 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEBSOCKET_INTERNAL_H
4 +#define NETDATA_WEBSOCKET_INTERNAL_H
5 +
6 +#include "websocket.h"
7 +
8 +// Maximum number of WebSocket threads
9 +#define WEBSOCKET_MAX_THREADS 2
10 +
11 +#define WORKERS_WEBSOCKET_POLL 0
12 +#define WORKERS_WEBSOCKET_CMD_READ 1
13 +#define WORKERS_WEBSOCKET_CMD_EXIT 2
14 +#define WORKERS_WEBSOCKET_CMD_ADD 3
15 +#define WORKERS_WEBSOCKET_CMD_DEL 4
16 +#define WORKERS_WEBSOCKET_CMD_BROADCAST 5
17 +#define WORKERS_WEBSOCKET_CMD_UNKNOWN 6
18 +#define WORKERS_WEBSOCKET_SOCK_RECEIVE 7
19 +#define WORKERS_WEBSOCKET_SOCK_SEND 8
20 +#define WORKERS_WEBSOCKET_SOCK_ERROR 9
21 +#define WORKERS_WEBSOCKET_CLIENT_TIMEOUT 10
22 +#define WORKERS_WEBSOCKET_SEND_PING 11
23 +#define WORKERS_WEBSOCKET_CLIENT_STUCK 12
24 +
25 +#define WORKERS_WEBSOCKET_INCOMPLETE_FRAME 13
26 +#define WORKERS_WEBSOCKET_COMPLETE_FRAME 14
27 +#define WORKERS_WEBSOCKET_MESSAGE 15
28 +#define WORKERS_WEBSOCKET_MSG_PING 16
29 +#define WORKERS_WEBSOCKET_MSG_PONG 17
30 +#define WORKERS_WEBSOCKET_MSG_CLOSE 18
31 +#define WORKERS_WEBSOCKET_MSG_INVALID 19
32 +
33 +// Forward declaration for thread structure
34 +struct websocket_thread;
35 +
36 +#include "websocket-compression.h"
37 +
38 +// WebSocket protocol constants
39 +#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
40 +
41 +// WebSocket frame constants
42 +#define WS_FIN 0x80 // Final frame bit
43 +#define WS_RSV1 0x40 // Reserved bit 1 (used for compression)
44 +#define WS_MASK 0x80 // Mask bit
45 +// Frame size limit - affects fragmentation but not total message size
46 +#define WS_MAX_FRAME_LENGTH (20 * 1024 * 1024) // 20MB max frame size
47 +
48 +// Total message size limits - these prevent resource exhaustion
49 +#define WEBSOCKET_MAX_COMPRESSED_SIZE (20ULL * 1024 * 1024) // 20MB max compressed message
50 +#define WEBSOCKET_MAX_UNCOMPRESSED_SIZE (200ULL * 1024 * 1024) // 200MB max uncompressed message
51 +
52 +// WebSocket frame header structure - used for processing frame headers
53 +typedef struct websocket_frame_header {
54 + unsigned char fin:1;
55 + unsigned char rsv1:1;
56 + unsigned char rsv2:1;
57 + unsigned char rsv3:1;
58 + unsigned char opcode:4;
59 + unsigned char mask:1;
60 + unsigned char len:7;
61 +
62 + unsigned char mask_key[4]; // Masking key (if present)
63 + size_t frame_size; // Size of the entire frame
64 + size_t header_size; // Size of the header
65 + size_t payload_length; // Length of the payload data
66 + void *payload; // Pointer to the payload data
67 +} WEBSOCKET_FRAME_HEADER;
68 +
69 +// Buffer for message data (used for reassembly of fragmented messages)
70 +typedef struct websocket_buffer {
71 + char *data; // Buffer holding message data
72 + size_t length; // Current buffer length
73 + size_t size; // Allocated buffer size
74 +} WS_BUF;
75 +
76 +// Forward declaration for client structure
77 +struct websocket_server_client;
78 +
79 +// Function prototypes for buffer handling
80 +
81 +// Message and payload processing functions
82 +void websocket_client_message_reset(struct websocket_server_client *wsc);
83 +bool websocket_client_process_message(struct websocket_server_client *wsc);
84 +bool websocket_client_decompress_message(struct websocket_server_client *wsc);
85 +
86 +// Additional helper functions
87 +bool websocket_frame_is_control_opcode(WEBSOCKET_OPCODE opcode);
88 +bool websocket_validate_utf8(const char *data, size_t length);
89 +
90 +#include "websocket-buffer.h"
91 +
92 +// WebSocket connection context - full structure definition
93 +struct websocket_server_client {
94 + WEBSOCKET_STATE state;
95 + ND_SOCK sock; // Socket with SSL abstraction
96 + uint32_t id; // Unique client ID
97 + size_t max_message_size;
98 + time_t connected_t; // Connection timestamp
99 + time_t last_activity_t; // Last activity timestamp
100 +
101 + // Buffer for I/O data
102 + struct circular_buffer in_buffer; // Incoming raw data (circular buffer)
103 + struct circular_buffer out_buffer; // Outgoing raw data (circular buffer)
104 + size_t next_frame_size; // The size of the next complete frame to read
105 +
106 + // Connection info
107 + char client_ip[INET6_ADDRSTRLEN];
108 + char client_port[NI_MAXSERV];
109 + WEBSOCKET_PROTOCOL protocol; // The negotiated subprotocol
110 +
111 + // Thread management
112 + struct websocket_thread *wth; // The thread handling this client
113 + struct websocket_server_client *prev; // Linked list for thread's client management
114 + struct websocket_server_client *next; // Linked list for thread's client management
115 +
116 + // Message processing state
117 + WS_BUF payload; // Pre-allocated buffer for message data
118 + WS_BUF u_payload; // Pre-allocated buffer for uncompressed message data
119 + WEBSOCKET_OPCODE opcode; // Current message opcode
120 + bool is_compressed; // Whether the current message is compressed
121 + bool message_complete; // Whether the current message is complete
122 + size_t message_id; // Sequential ID for messages, starting from 0
123 + size_t frame_id; // Sequential ID for frames within current message
124 +
125 + // Compression state
126 + WEBSOCKET_COMPRESSION_CTX compression;
127 +
128 + // Connection closing state
129 + bool flush_and_remove_client; // Flag to indicate we're just flushing buffer before close
130 +
131 + // Protocol handler callbacks
132 + void (*on_connect)(struct websocket_server_client *wsc); // Called when a client is successfully connected
133 + void (*on_message)(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode); // Called when a message is received
134 + void (*on_close)(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason); // Called BEFORE sending close frame
135 + void (*on_disconnect)(struct websocket_server_client *wsc); // Called when a client is disconnected
136 +
137 + // User data for application use
138 + void *user_data;
139 +};
140 +
141 +// Forward declarations for websocket client
142 +typedef struct websocket_server_client WS_CLIENT;
143 +
144 +// WebSocket thread structure
145 +typedef struct websocket_thread {
146 + size_t id; // Thread ID
147 + pid_t tid;
148 +
149 + struct {
150 + ND_THREAD *thread; // Thread handle
151 + bool running; // Thread running status
152 + SPINLOCK spinlock; // Thread spinlock
153 + };
154 +
155 + size_t clients_current; // Current number of clients in the thread
156 + SPINLOCK clients_spinlock; // Spinlock for client operations
157 + struct websocket_server_client *clients; // Head of the clients double-linked list
158 +
159 + nd_poll_t *ndpl; // Poll instance
160 +
161 + struct {
162 + int pipe[2]; // Command pipe [0] = read, [1] = write
163 + } cmd;
164 +
165 +} WEBSOCKET_THREAD;
166 +
167 +// Global array of WebSocket threads
168 +extern WEBSOCKET_THREAD websocket_threads[WEBSOCKET_MAX_THREADS];
169 +
170 +// Define JudyL typed structure for WebSocket clients
171 +DEFINE_JUDYL_TYPED(WS_CLIENTS, struct websocket_server_client *);
172 +
173 +// WebSocket thread commands
174 +#define WEBSOCKET_THREAD_CMD_EXIT 1
175 +#define WEBSOCKET_THREAD_CMD_ADD_CLIENT 2
176 +#define WEBSOCKET_THREAD_CMD_REMOVE_CLIENT 3
177 +#define WEBSOCKET_THREAD_CMD_BROADCAST 4
178 +
179 +// Buffer size definitions for WebSocket operations
180 +#define WEBSOCKET_RECEIVE_BUFFER_SIZE 4096 // Size used for network read operations
181 +
182 +// Initial buffer sizes
183 +#define WEBSOCKET_IN_BUFFER_INITIAL_SIZE 8192UL // Initial size for incoming data buffer
184 +#define WEBSOCKET_OUT_BUFFER_INITIAL_SIZE 16384UL // Initial size for outgoing data buffer
185 +#define WEBSOCKET_PAYLOAD_INITIAL_SIZE 8192UL // Initial size for message payload buffer
186 +#define WEBSOCKET_UNPACKED_INITIAL_SIZE 16384UL // Initial size for uncompressed message buffer
187 +
188 +// Maximum buffer sizes to protect against memory exhaustion
189 +#define WEBSOCKET_IN_BUFFER_MAX_SIZE (20UL * 1024 * 1024) // 10MiB max for incoming data buffer
190 +#define WEBSOCKET_OUT_BUFFER_MAX_SIZE (20UL * 1024 * 1024) // 10MiB max for outgoing data buffer
191 +
192 +NEVERNULL
193 +WS_CLIENT *websocket_client_create(void);
194 +
195 +// Thread management
196 +void websocket_threads_init(void);
197 +void websocket_threads_join(void);
198 +bool websocket_thread_send_command(WEBSOCKET_THREAD *wth, uint8_t cmd, uint32_t id);
199 +bool websocket_thread_send_broadcast(WEBSOCKET_THREAD *wth, WEBSOCKET_OPCODE opcode, const char *message);
200 +void *websocket_thread(void *ptr);
201 +void websocket_thread_enqueue_client(WEBSOCKET_THREAD *wth, struct websocket_server_client *wsc);
202 +bool websocket_thread_update_client_poll_flags(struct websocket_server_client *wsc);
203 +
204 +// Client registry internals
205 +void websocket_client_free(WS_CLIENT *wsc);
206 +bool websocket_client_register(struct websocket_server_client *wsc);
207 +void websocket_client_unregister(struct websocket_server_client *wsc);
208 +struct websocket_server_client *websocket_client_find_by_id(size_t id);
209 +
210 +// Utility functions
211 +// Validates a WebSocket close code according to RFC 6455
212 +bool websocket_validate_close_code(uint16_t code);
213 +void websocket_debug(WS_CLIENT *wsc, const char *format, ...);
214 +void websocket_info(WS_CLIENT *wsc, const char *format, ...);
215 +void websocket_error(WS_CLIENT *wsc, const char *format, ...);
216 +void websocket_dump_debug(WS_CLIENT *wsc, const char *payload, size_t payload_length, const char *format, ...);
217 +
218 +// Frame processing result codes
219 +typedef enum {
220 + WS_FRAME_ERROR = -1, // Processing error occurred
221 + WS_FRAME_COMPLETE = 0, // Frame processing completed successfully
222 + WS_FRAME_NEED_MORE_DATA = 1, // Need more data to complete frame processing
223 + WS_FRAME_MESSAGE_READY = 2 // A complete message is ready to be processed
224 +} WEBSOCKET_FRAME_RESULT;
225 +
226 +// Centralized protocol validation functions
227 +void websocket_protocol_exception(WS_CLIENT *wsc, WEBSOCKET_CLOSE_CODE reason_code, const char *reason_txt);
228 +
229 +// Protocol receiver functions - websocket-protocol-rcv.c
230 +ssize_t websocket_protocol_got_data(WS_CLIENT *wsc, char *data, size_t length);
231 +
232 +// Protocol sender functions - websocket-protocol-snd.c
233 +int websocket_protocol_send_frame(
234 + WS_CLIENT *wsc, const char *payload,
235 + size_t payload_len, WEBSOCKET_OPCODE opcode, bool use_compression);
236 +int websocket_protocol_send_text(WS_CLIENT *wsc, const char *text);
237 +int websocket_protocol_send_binary(WS_CLIENT *wsc, const void *data, size_t length);
238 +int websocket_protocol_send_close(WS_CLIENT *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason);
239 +int websocket_protocol_send_ping(WS_CLIENT *wsc, const char *data, size_t length);
240 +int websocket_protocol_send_pong(WS_CLIENT *wsc, const char *data, size_t length);
241 +
242 +// IO functions from old implementation - will be refactored
243 +ssize_t websocket_receive_data(struct websocket_server_client *wsc);
244 +ssize_t websocket_write_data(struct websocket_server_client *wsc);
245 +
246 +// WebSocket message sending functions
247 +int websocket_send_message(WS_CLIENT *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode);
248 +int websocket_broadcast_message(const char *message, WEBSOCKET_OPCODE opcode);
249 +
250 +bool websocket_protocol_parse_header_from_buffer(const char *buffer, size_t length,
251 + WEBSOCKET_FRAME_HEADER *header);
252 +#endif // NETDATA_WEBSOCKET_INTERNAL_H
\ No newline at end of file
src/web/websocket/websocket-jsonrpc.c new
+311
@@ -0,0 +1,311 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-jsonrpc.h"
4 +
5 +static int websocket_client_send_json(struct websocket_server_client *wsc, struct json_object *json) {
6 + if (!wsc || !json)
7 + return -1;
8 +
9 + websocket_debug(wsc, "Sending JSON message");
10 +
11 + // Convert JSON to string
12 + const char *json_str = json_object_to_json_string_ext(json, JSON_C_TO_STRING_PLAIN);
13 + if (!json_str) {
14 + websocket_error(wsc, "Failed to convert JSON to string");
15 + return -1;
16 + }
17 +
18 + // Send as text message
19 + int result = websocket_protocol_send_text(wsc, json_str);
20 +
21 + websocket_debug(wsc, "Sent JSON message, result=%d", result);
22 + return result;
23 +}
24 +
25 +// Called when a client is connected and ready to exchange messages
26 +void jsonrpc_on_connect(struct websocket_server_client *wsc) {
27 + if (!wsc) return;
28 +
29 + websocket_debug(wsc, "JSON-RPC client connected");
30 +
31 + // Future implementation - can send a welcome message or initialize client state
32 +}
33 +
34 +// Called when a client is about to be disconnected
35 +void jsonrpc_on_disconnect(struct websocket_server_client *wsc) {
36 + if (!wsc) return;
37 +
38 + websocket_debug(wsc, "JSON-RPC client disconnected");
39 +
40 + // Future implementation - can clean up any client-specific resources
41 +}
42 +
43 +// Called before sending a close frame to the client
44 +void jsonrpc_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason) {
45 + if (!wsc) return;
46 +
47 + websocket_debug(wsc, "JSON-RPC client closing with code %d (%s): %s",
48 + code,
49 + code == WS_CLOSE_NORMAL ? "Normal" :
50 + code == WS_CLOSE_GOING_AWAY ? "Going Away" :
51 + code == WS_CLOSE_PROTOCOL_ERROR ? "Protocol Error" :
52 + code == WS_CLOSE_INTERNAL_ERROR ? "Internal Error" : "Other",
53 + reason ? reason : "No reason provided");
54 +
55 + // Future implementation - can send a final message before closing
56 +}
57 +
58 +// Adapter function for the on_message callback to match WS_CLIENT callback signature
59 +void jsonrpc_on_message_callback(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode) {
60 + if (!wsc || !message || length == 0)
61 + return;
62 +
63 + // JSON-RPC only works with text messages
64 + if (opcode != WS_OPCODE_TEXT) {
65 + websocket_error(wsc, "JSON-RPC protocol received non-text message, ignoring");
66 + return;
67 + }
68 +
69 + websocket_debug(wsc, "JSON-RPC callback processing message: length=%zu", length);
70 +
71 + // Process the message
72 + websocket_jsonrpc_process_message(wsc, message, length);
73 +}
74 +
75 +// Utility function to extract parameters from a request
76 +struct json_object *websocket_jsonrpc_get_params(struct json_object *request) {
77 + if (!request)
78 + return NULL;
79 +
80 + struct json_object *params = NULL;
81 + if (json_object_object_get_ex(request, "params", &params)) {
82 + return params; // Return the params object if it exists
83 + }
84 +
85 + return NULL; // No params found
86 +}
87 +
88 +// Handler for the "echo" method - simply returns the params as the result
89 +static void jsonrpc_echo_handler(WS_CLIENT *wsc, struct json_object *request, uint64_t id) {
90 + // Get the params if available
91 + struct json_object *params = websocket_jsonrpc_get_params(request);
92 +
93 + // Clone the params to avoid ownership issues
94 + struct json_object *result = params ? json_object_get(params) : json_object_new_object();
95 +
96 + // Send response
97 + websocket_jsonrpc_response_result(wsc, result, id);
98 +}
99 +
100 +// Define a fixed array of method handlers
101 +static struct {
102 + const char *method;
103 + jsonrpc_method_handler handler;
104 +} jsonrpc_methods[] = {
105 + { "echo", jsonrpc_echo_handler },
106 +
107 + // Add more methods here as needed
108 + // { "method_name", method_handler_function },
109 +
110 + // Terminator
111 + { NULL, NULL }
112 +};
113 +
114 +// Initialize the JSON-RPC protocol
115 +void websocket_jsonrpc_initialize(void) {
116 + netdata_log_info("JSON-RPC protocol initialized with built-in methods");
117 +}
118 +
119 +// Find a method handler
120 +static jsonrpc_method_handler find_method_handler(const char *method) {
121 + if (!method)
122 + return NULL;
123 +
124 + // Simple linear search through the fixed array
125 + for (int i = 0; jsonrpc_methods[i].method != NULL; i++) {
126 + if (strcmp(jsonrpc_methods[i].method, method) == 0) {
127 + return jsonrpc_methods[i].handler;
128 + }
129 + }
130 +
131 + return NULL;
132 +}
133 +
134 +// Validate JSON-RPC request according to specification
135 +bool websocket_jsonrpc_validate_request(struct json_object *request) {
136 + if (!request || json_object_get_type(request) != json_type_object)
137 + return false;
138 +
139 + // Check for required fields
140 + struct json_object *jsonrpc, *method;
141 +
142 + if (!json_object_object_get_ex(request, "jsonrpc", &jsonrpc) ||
143 + !json_object_object_get_ex(request, "method", &method))
144 + return false;
145 +
146 + // Validate jsonrpc version
147 + if (json_object_get_type(jsonrpc) != json_type_string ||
148 + strcmp(json_object_get_string(jsonrpc), JSONRPC_VERSION) != 0)
149 + return false;
150 +
151 + // Validate method
152 + if (json_object_get_type(method) != json_type_string)
153 + return false;
154 +
155 + return true;
156 +}
157 +
158 +// Process a JSON-RPC request
159 +static void process_jsonrpc_request(WS_CLIENT *wsc, struct json_object *request) {
160 + if (!websocket_jsonrpc_validate_request(request)) {
161 + websocket_jsonrpc_response_error(wsc, JSONRPC_ERROR_INVALID_REQUEST,
162 + "Invalid JSON-RPC request", 0);
163 + return;
164 + }
165 +
166 + // Extract request components
167 + struct json_object *method_obj, *id_obj = NULL;
168 +
169 + json_object_object_get_ex(request, "method", &method_obj);
170 + const char *method = json_object_get_string(method_obj);
171 +
172 + // Get ID if present (0 indicates a notification that requires no response)
173 + uint64_t id = 0;
174 + bool has_id = json_object_object_get_ex(request, "id", &id_obj);
175 + if (has_id && id_obj && json_object_get_type(id_obj) != json_type_null) {
176 + if (json_object_get_type(id_obj) == json_type_int)
177 + id = json_object_get_int64(id_obj);
178 + else if (json_object_get_type(id_obj) == json_type_string) {
179 + // Try to convert string ID to integer if possible
180 + const char *id_str = json_object_get_string(id_obj);
181 + char *endptr;
182 + id = (uint64_t)strtoll(id_str, &endptr, 10);
183 + if (*endptr != '\0') {
184 + // Not a number, just hash the string for an ID
185 + id = simple_hash(id_str);
186 + }
187 + }
188 + }
189 +
190 + // Find handler for the requested method
191 + jsonrpc_method_handler handler = find_method_handler(method);
192 + if (!handler) {
193 + if (has_id) {
194 + websocket_jsonrpc_response_error(wsc, JSONRPC_ERROR_METHOD_NOT_FOUND,
195 + "Method not found", id);
196 + }
197 + return;
198 + }
199 +
200 + // Call the handler with the request
201 + handler(wsc, request, id);
202 +}
203 +
204 +// Process a WebSocket message as JSON-RPC
205 +bool websocket_jsonrpc_process_message(WS_CLIENT *wsc, const char *message, size_t length) {
206 + if (!wsc || !message || length == 0)
207 + return false;
208 +
209 + websocket_debug(wsc, "Processing JSON-RPC message: length=%zu", length);
210 +
211 + // Parse the JSON
212 + struct json_object *json = json_tokener_parse(message);
213 + if (!json) {
214 + websocket_error(wsc, "Failed to parse JSON-RPC message");
215 + websocket_jsonrpc_response_error(wsc, JSONRPC_ERROR_PARSE_ERROR,
216 + "Parse error", 0);
217 + return false;
218 + }
219 +
220 + // Process based on message type
221 + if (json_object_get_type(json) == json_type_array) {
222 + // Batch request
223 + websocket_debug(wsc, "Processing JSON-RPC batch request");
224 +
225 + int array_len = json_object_array_length(json);
226 + for (int i = 0; i < array_len; i++) {
227 + struct json_object *request = json_object_array_get_idx(json, i);
228 + process_jsonrpc_request(wsc, request);
229 + }
230 + }
231 + else if (json_object_get_type(json) == json_type_object) {
232 + // Single request
233 + process_jsonrpc_request(wsc, json);
234 + }
235 + else {
236 + // Invalid request
237 + websocket_jsonrpc_response_error(wsc, JSONRPC_ERROR_INVALID_REQUEST,
238 + "Invalid request", 0);
239 + json_object_put(json);
240 + return false;
241 + }
242 +
243 + json_object_put(json);
244 + return true;
245 +}
246 +
247 +// Create and send a JSON-RPC success response
248 +void websocket_jsonrpc_response_result(WS_CLIENT *wsc, struct json_object *result, uint64_t id) {
249 + if (!wsc || id == 0) // No response for notifications (id == 0)
250 + return;
251 +
252 + struct json_object *response = json_object_new_object();
253 +
254 + // Add required fields
255 + json_object_object_add(response, "jsonrpc", json_object_new_string(JSONRPC_VERSION));
256 +
257 + // Add result (takes ownership of the result object)
258 + if (result) {
259 + json_object_object_add(response, "result", result);
260 + } else {
261 + json_object_object_add(response, "result", json_object_new_object());
262 + }
263 +
264 + // Add ID
265 + json_object_object_add(response, "id", json_object_new_int64(id));
266 +
267 + // Send the response
268 + websocket_client_send_json(wsc, response);
269 +
270 + // Free the response object
271 + json_object_put(response);
272 +}
273 +
274 +// Create and send a JSON-RPC error response
275 +void websocket_jsonrpc_response_error(WS_CLIENT *wsc, JSONRPC_ERROR_CODE code, const char *message, uint64_t id) {
276 + websocket_jsonrpc_response_error_with_data(wsc, code, message, NULL, id);
277 +}
278 +
279 +// Create and send a JSON-RPC error response with additional data
280 +void websocket_jsonrpc_response_error_with_data(WS_CLIENT *wsc, JSONRPC_ERROR_CODE code, const char *message,
281 + struct json_object *data, uint64_t id) {
282 + if (!wsc || id == 0) // No response for notifications (id == 0)
283 + return;
284 +
285 + struct json_object *response = json_object_new_object();
286 + struct json_object *error = json_object_new_object();
287 +
288 + // Add required fields
289 + json_object_object_add(response, "jsonrpc", json_object_new_string(JSONRPC_VERSION));
290 +
291 + // Add error code and message
292 + json_object_object_add(error, "code", json_object_new_int(code));
293 + json_object_object_add(error, "message", json_object_new_string(message ? message : "Unknown error"));
294 +
295 + // Add error data if provided
296 + if (data) {
297 + json_object_object_add(error, "data", data);
298 + }
299 +
300 + // Add error object to response
301 + json_object_object_add(response, "error", error);
302 +
303 + // Add ID
304 + json_object_object_add(response, "id", json_object_new_int64(id));
305 +
306 + // Send the response
307 + websocket_client_send_json(wsc, response);
308 +
309 + // Free the response object
310 + json_object_put(response);
311 +}
src/web/websocket/websocket-jsonrpc.h new
+56
@@ -0,0 +1,56 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEBSOCKET_JSONRPC_H
4 +#define NETDATA_WEBSOCKET_JSONRPC_H
5 +
6 +#include "websocket-internal.h"
7 +
8 +// JSON-RPC 2.0 protocol constants
9 +#define JSONRPC_VERSION "2.0"
10 +
11 +// JSON-RPC error codes as per specification
12 +typedef enum {
13 + // Official JSON-RPC 2.0 error codes
14 + JSONRPC_ERROR_PARSE_ERROR = -32700, // Invalid JSON was received by the server
15 + JSONRPC_ERROR_INVALID_REQUEST = -32600, // The JSON sent is not a valid Request object
16 + JSONRPC_ERROR_METHOD_NOT_FOUND = -32601, // The method does not exist / is not available
17 + JSONRPC_ERROR_INVALID_PARAMS = -32602, // Invalid method parameter(s)
18 + JSONRPC_ERROR_INTERNAL_ERROR = -32603, // Internal JSON-RPC error
19 +
20 + // -32000 to -32099 are reserved for implementation-defined server-errors
21 + JSONRPC_ERROR_SERVER_ERROR = -32000, // Generic server error
22 +
23 + // Netdata specific error codes (using reserved server-error range)
24 + JSONRPC_ERROR_NETDATA_PERMISSION_DENIED = -32030, // Permission denied
25 + JSONRPC_ERROR_NETDATA_NOT_SUPPORTED = -32031, // Feature not supported
26 + JSONRPC_ERROR_NETDATA_RATE_LIMIT = -32032, // Rate limit exceeded
27 +} JSONRPC_ERROR_CODE;
28 +
29 +// Method handler function type
30 +typedef void (*jsonrpc_method_handler)(WS_CLIENT *wsc, struct json_object *request, uint64_t id);
31 +
32 +// Initialize WebSocket JSON-RPC protocol
33 +void websocket_jsonrpc_initialize(void);
34 +
35 +// Process a WebSocket message as JSON-RPC
36 +bool websocket_jsonrpc_process_message(WS_CLIENT *wsc, const char *message, size_t length);
37 +
38 +// WebSocket protocol handler callbacks for JSON-RPC
39 +void jsonrpc_on_connect(struct websocket_server_client *wsc);
40 +void jsonrpc_on_message_callback(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode);
41 +void jsonrpc_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason);
42 +void jsonrpc_on_disconnect(struct websocket_server_client *wsc);
43 +
44 +// Response functions
45 +void websocket_jsonrpc_response_result(WS_CLIENT *wsc, struct json_object *result, uint64_t id);
46 +void websocket_jsonrpc_response_error(WS_CLIENT *wsc, JSONRPC_ERROR_CODE code, const char *message, uint64_t id);
47 +void websocket_jsonrpc_response_error_with_data(WS_CLIENT *wsc, JSONRPC_ERROR_CODE code, const char *message,
48 + struct json_object *data, uint64_t id);
49 +
50 +// Helper functions
51 +bool websocket_jsonrpc_validate_request(struct json_object *request);
52 +
53 +// Utility function to extract parameters from a request
54 +struct json_object *websocket_jsonrpc_get_params(struct json_object *request);
55 +
56 +#endif // NETDATA_WEBSOCKET_JSONRPC_H
src/web/websocket/websocket-message.c new
+190
@@ -0,0 +1,190 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-internal.h"
4 +
5 +// Helper function to determine if an opcode is a control opcode
6 +bool websocket_frame_is_control_opcode(WEBSOCKET_OPCODE opcode) {
7 + return (opcode == WS_OPCODE_CLOSE ||
8 + opcode == WS_OPCODE_PING ||
9 + opcode == WS_OPCODE_PONG);
10 +}
11 +
12 +// Validates that a buffer contains valid UTF-8 encoded data
13 +// Returns true if the data is valid UTF-8, false otherwise
14 +bool websocket_validate_utf8(const char *data, size_t length) {
15 + if (!data)
16 + return length == 0; // Empty data is valid
17 +
18 + const unsigned char *bytes = (const unsigned char *)data;
19 + size_t i = 0;
20 +
21 + while (i < length) {
22 + // Check for ASCII (single-byte character)
23 + if (bytes[i] <= 0x7F) {
24 + i++;
25 + continue;
26 + }
27 +
28 + // Check for 2-byte sequence
29 + else if ((bytes[i] & 0xE0) == 0xC0) {
30 + // Need at least 2 bytes
31 + if (i + 1 >= length)
32 + return false;
33 +
34 + // Second byte must be a continuation byte
35 + if ((bytes[i+1] & 0xC0) != 0x80)
36 + return false;
37 +
38 + // Must not be overlong encoding
39 + if (bytes[i] < 0xC2)
40 + return false;
41 +
42 + i += 2;
43 + }
44 +
45 + // Check for 3-byte sequence
46 + else if ((bytes[i] & 0xF0) == 0xE0) {
47 + // Need at least 3 bytes
48 + if (i + 2 >= length)
49 + return false;
50 +
51 + // Second and third bytes must be continuation bytes
52 + if ((bytes[i+1] & 0xC0) != 0x80 || (bytes[i+2] & 0xC0) != 0x80)
53 + return false;
54 +
55 + // Check for overlong encoding
56 + if (bytes[i] == 0xE0 && (bytes[i+1] & 0xE0) == 0x80)
57 + return false;
58 +
59 + // Check for UTF-16 surrogates (not allowed in UTF-8)
60 + if (bytes[i] == 0xED && (bytes[i+1] & 0xE0) == 0xA0)
61 + return false;
62 +
63 + i += 3;
64 + }
65 +
66 + // Check for 4-byte sequence
67 + else if ((bytes[i] & 0xF8) == 0xF0) {
68 + // Need at least 4 bytes
69 + if (i + 3 >= length)
70 + return false;
71 +
72 + // Second, third, and fourth bytes must be continuation bytes
73 + if ((bytes[i+1] & 0xC0) != 0x80 ||
74 + (bytes[i+2] & 0xC0) != 0x80 ||
75 + (bytes[i+3] & 0xC0) != 0x80)
76 + return false;
77 +
78 + // Check for overlong encoding
79 + if (bytes[i] == 0xF0 && (bytes[i+1] & 0xF0) == 0x80)
80 + return false;
81 +
82 + // Check for values outside Unicode range
83 + if (bytes[i] > 0xF4 || (bytes[i] == 0xF4 && bytes[i+1] > 0x8F))
84 + return false;
85 +
86 + i += 4;
87 + }
88 +
89 + // Invalid UTF-8 leading byte
90 + else {
91 + return false;
92 + }
93 + }
94 +
95 + return true;
96 +}
97 +
98 +// Reset a client's message state for a new message
99 +void websocket_client_message_reset(WS_CLIENT *wsc) {
100 + if (!wsc)
101 + return;
102 +
103 + // Reset message buffer
104 + wsb_reset(&wsc->payload);
105 +
106 + // Also reset uncompressed buffer to avoid keeping stale data
107 + wsb_reset(&wsc->u_payload);
108 +
109 + // Reset client's message state
110 + // We set message_complete to true by default (no fragmented message in progress),
111 + // but this will be overridden based on the FIN bit for actual frames
112 + wsc->message_complete = true;
113 + wsc->is_compressed = false;
114 + wsc->opcode = WS_OPCODE_TEXT; // Default opcode
115 + wsc->frame_id = 0;
116 +}
117 +
118 +// Process a complete message (decompress if needed and call handler)
119 +bool websocket_client_process_message(WS_CLIENT *wsc) {
120 + if (!wsc || !wsc->message_complete)
121 + return false;
122 +
123 + worker_is_busy(WORKERS_WEBSOCKET_MESSAGE);
124 +
125 + websocket_debug(wsc, "Processing message (opcode=0x%x, is_compressed=%d, length=%zu)",
126 + wsc->opcode, wsc->is_compressed,
127 + wsb_length(&wsc->payload));
128 +
129 + // Handle control frames immediately
130 + if (wsc->opcode != WS_OPCODE_TEXT && wsc->opcode != WS_OPCODE_BINARY) {
131 + websocket_debug(wsc, "Control frame (opcode=0x%x) should not be handled by %s()", wsc->opcode, __FUNCTION__);
132 + return false;
133 + }
134 +
135 + // At this point, we know we're dealing with a data frame (text or binary)
136 + WS_BUF *wsb;
137 +
138 + // Handle decompression if needed
139 + if (wsc->is_compressed) {
140 + if (!websocket_client_decompress_message(wsc)) {
141 + websocket_protocol_exception(wsc, WS_CLOSE_INTERNAL_ERROR, "Decompression failed");
142 + return false;
143 + }
144 + wsb = &wsc->u_payload;
145 + }
146 + else
147 + wsb = &wsc->payload;
148 +
149 + // For uncompressed messages, we just use payload buffer directly
150 + if (wsc->opcode == WS_OPCODE_TEXT) {
151 + wsb_null_terminate(wsb);
152 +
153 + if (!websocket_validate_utf8(wsb_data(wsb), wsb_length(wsb))) {
154 + websocket_protocol_exception(wsc, WS_CLOSE_INVALID_PAYLOAD,
155 + "Invalid UTF-8 data in text message");
156 + return false;
157 + }
158 + }
159 +
160 + // Now handle the uncompressed message - using the new function
161 + // that contains the actual handler logic
162 +
163 + websocket_debug(wsc, "Handling message: type=%s, length=%zu, protocol=%d",
164 + (wsc->opcode == WS_OPCODE_BINARY) ? "binary" : "text",
165 + wsb_length(wsb), wsc->protocol);
166 +
167 + // Ensure text messages are null-terminated
168 + if (wsc->opcode == WS_OPCODE_TEXT)
169 + wsb_null_terminate(wsb);
170 +
171 + // Call the message callback if set - this allows protocols to be handled dynamically
172 + if (wsc->on_message) {
173 + websocket_debug(wsc, "Calling client message handler for protocol %d", wsc->protocol);
174 + wsc->on_message(wsc, wsb_data(wsb), wsb_length(wsb), wsc->opcode);
175 + }
176 + else {
177 + // No handler registered - this should not happen as we check during handshake
178 + websocket_error(wsc, "No message handler registered for protocol %d", wsc->protocol);
179 + return false;
180 + }
181 +
182 + // Update client message stats
183 + wsc->message_id++;
184 + wsc->frame_id = 0;
185 +
186 + // Reset for the next message
187 + websocket_client_message_reset(wsc);
188 +
189 + return true;
190 +}
src/web/websocket/websocket-receive.c new
+882
@@ -0,0 +1,882 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-internal.h"
4 +
5 +// --------------------------------------------------------------------------------------------------------------------
6 +// reading from the socket
7 +
8 +static inline bool cbuffer_has_enough_data_for_next_frame(WS_CLIENT *wsc) {
9 + return wsc->next_frame_size > 0 &&
10 + cbuffer_used_size_unsafe(&wsc->in_buffer) >= wsc->next_frame_size;
11 +}
12 +
13 +static inline bool cbuffer_next_frame_is_fragmented(WS_CLIENT *wsc) {
14 + return cbuffer_has_enough_data_for_next_frame(wsc) &&
15 + cbuffer_next_unsafe(&wsc->in_buffer, NULL) < wsc->next_frame_size;
16 +}
17 +
18 +static ssize_t websocket_received_data_process(WS_CLIENT *wsc, ssize_t bytes_read) {
19 + if(cbuffer_next_frame_is_fragmented(wsc))
20 + cbuffer_ensure_unwrapped_size(&wsc->in_buffer, wsc->next_frame_size);
21 +
22 + char *buffer_pos;
23 + size_t contiguous_input = cbuffer_next_unsafe(&wsc->in_buffer, &buffer_pos);
24 +
25 + // Now we have contiguous data for processing
26 + ssize_t bytes_consumed = websocket_protocol_got_data(wsc, buffer_pos, contiguous_input);
27 + if (bytes_consumed < 0) {
28 + if (bytes_consumed < -1) {
29 + bytes_consumed = -bytes_consumed;
30 + cbuffer_remove_unsafe(&wsc->in_buffer, bytes_consumed);
31 + }
32 +
33 + websocket_error(wsc, "Failed to process received data");
34 + return -1;
35 + }
36 +
37 + // Check if bytes_processed is 0 but this was a successful call
38 + // This means we have an incomplete frame and need to keep the entire buffer
39 + if (bytes_consumed == 0) {
40 + websocket_debug(
41 + wsc, "Incomplete frame detected - keeping all %zu bytes in buffer for next read", contiguous_input);
42 + return bytes_read; // Return the bytes read so caller knows we made progress
43 + }
44 +
45 + // We've processed some data - remove it from the circular buffer
46 + cbuffer_remove_unsafe(&wsc->in_buffer, bytes_consumed);
47 +
48 + return bytes_consumed;
49 +}
50 +
51 +// Process incoming WebSocket data
52 +ssize_t websocket_receive_data(WS_CLIENT *wsc) {
53 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
54 +
55 + worker_is_busy(WORKERS_WEBSOCKET_SOCK_RECEIVE);
56 +
57 + if (!wsc->in_buffer.data || wsc->sock.fd < 0)
58 + return -1;
59 +
60 + size_t available_space = WEBSOCKET_RECEIVE_BUFFER_SIZE;
61 + if(wsc->next_frame_size > 0) {
62 + size_t used_space = cbuffer_used_size_unsafe(&wsc->in_buffer);
63 + if(used_space < wsc->next_frame_size) {
64 + size_t missing_for_next_frame = wsc->next_frame_size - used_space;
65 + available_space = MAX(missing_for_next_frame, WEBSOCKET_RECEIVE_BUFFER_SIZE);
66 + }
67 + }
68 +
69 + char *buffer = cbuffer_reserve_unsafe(&wsc->in_buffer, available_space);
70 + if(!buffer) {
71 + websocket_error(wsc, "Not enough space to read %zu bytes", available_space);
72 + return -1;
73 + }
74 +
75 + // Read data from socket into temporary buffer using ND_SOCK
76 + ssize_t bytes_read = nd_sock_read(&wsc->sock, buffer, available_space, 0);
77 +
78 + if (bytes_read <= 0) {
79 + if (bytes_read == 0) {
80 + // Connection closed
81 + websocket_debug(wsc, "Client closed connection");
82 + return -1;
83 + }
84 +
85 + if (errno == EAGAIN || errno == EWOULDBLOCK)
86 + return 0; // No data available right now
87 +
88 + websocket_error(wsc, "Failed to read from client: %s", strerror(errno));
89 + return -1;
90 + }
91 +
92 + if (bytes_read > (ssize_t)available_space) {
93 + websocket_error(wsc, "Received more data (%zd) than available space in buffer (%zd)",
94 + bytes_read, available_space);
95 + return -1;
96 + }
97 +
98 + cbuffer_commit_reserved_unsafe(&wsc->in_buffer, bytes_read);
99 +
100 + // Update last activity time
101 + wsc->last_activity_t = now_monotonic_sec();
102 +
103 + // Dump the received data for debugging
104 + websocket_dump_debug(wsc, buffer, bytes_read, "RX SOCK %zd bytes", bytes_read);
105 +
106 + if(wsc->next_frame_size == 0 || cbuffer_has_enough_data_for_next_frame(wsc)) {
107 + // we don't know the next frame size
108 + // or, we know it and we have all the data for it
109 +
110 + // process the received data
111 + if(websocket_received_data_process(wsc, bytes_read) < 0)
112 + return -1;
113 +
114 + // we may still have wrapped data in the circular buffer that can satisfy the entire next frame
115 + if(cbuffer_next_frame_is_fragmented(wsc)) {
116 + // we have enough data to process this frame, no need to wait for more input
117 + if(websocket_received_data_process(wsc, bytes_read) < 0)
118 + return -1;
119 + }
120 + }
121 +
122 + // Return the number of bytes we processed from this read
123 + // Even if bytes_processed is 0, we still read data which will be processed later
124 + return bytes_read;
125 +}
126 +
127 +// --------------------------------------------------------------------------------------------------------------------
128 +
129 +// Validate a WebSocket close code according to RFC 6455
130 +bool websocket_validate_close_code(uint16_t code) {
131 + // 1000-2999 are reserved for the WebSocket protocol
132 + // 3000-3999 are reserved for use by libraries, frameworks, and applications
133 + // 4000-4999 are reserved for private use
134 +
135 + // Check if code is in valid ranges
136 + if ((code >= 1000 && code <= 1011) || // Protocol-defined codes
137 + (code >= 3000 && code <= 4999)) // Application/library/private codes
138 + {
139 + // Codes 1004, 1005, and 1006 must not be used in a Close frame by an endpoint
140 + if (code != WS_CLOSE_RESERVED && code != WS_CLOSE_NO_STATUS && code != WS_CLOSE_ABNORMAL)
141 + return true;
142 + }
143 +
144 + return false;
145 +}
146 +
147 +// Centralized function to handle WebSocket protocol exceptions
148 +void websocket_protocol_exception(WS_CLIENT *wsc, WEBSOCKET_CLOSE_CODE reason_code, const char *reason_txt) {
149 + if (!wsc) return;
150 +
151 + websocket_error(wsc, "Protocol exception: %s (code: %d, %s)",
152 + reason_txt, reason_code, WEBSOCKET_CLOSE_CODE_2str(reason_code));
153 +
154 + // Always send a close frame with the reason
155 + websocket_protocol_send_close(wsc, reason_code, reason_txt);
156 +
157 + // Update state based on current state
158 + if (wsc->state == WS_STATE_OPEN) {
159 + // We're initiating the close - transition to server-initiated closing
160 + wsc->state = WS_STATE_CLOSING_SERVER;
161 + }
162 + else if (wsc->state == WS_STATE_CLOSING_CLIENT || wsc->state == WS_STATE_CLOSING_SERVER) {
163 + // Already in closing state, nothing to do
164 + websocket_debug(wsc, "Protocol exception during closing state %s",
165 + WEBSOCKET_STATE_2str(wsc->state));
166 + }
167 + else {
168 + // For any other state, move straight to CLOSED
169 + wsc->state = WS_STATE_CLOSED;
170 + }
171 +
172 + // For severe protocol errors, force immediate disconnection
173 + if (reason_code == WS_CLOSE_PROTOCOL_ERROR ||
174 + reason_code == WS_CLOSE_POLICY_VIOLATION ||
175 + reason_code == WS_CLOSE_INVALID_PAYLOAD) {
176 +
177 + websocket_info(wsc, "Forcing immediate disconnection due to protocol exception");
178 +
179 + // First try to flush outgoing data to send the close frame
180 + websocket_write_data(wsc);
181 +
182 + // Remove client from thread
183 + if (wsc->wth) {
184 + websocket_thread_send_command(wsc->wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
185 + }
186 + }
187 +}
188 +
189 +#define WS_ALLOW_USE ( 1)
190 +#define WS_ALLOW_DISCARD ( 0)
191 +#define WS_ALLOW_ERROR (-1)
192 +
193 +// Centralized function to check if a frame is allowed based on connection state
194 +static int websocket_is_frame_allowed(WS_CLIENT *wsc, const WEBSOCKET_FRAME_HEADER *header) {
195 + if (!wsc || !header)
196 + return WS_ALLOW_ERROR;
197 +
198 + bool is_control = websocket_frame_is_control_opcode(header->opcode);
199 +
200 + // Check state-based restrictions
201 + switch (wsc->state) {
202 + case WS_STATE_OPEN:
203 + // In OPEN state, all frames are allowed
204 + return WS_ALLOW_USE;
205 +
206 + case WS_STATE_CLOSING_SERVER:
207 + // When server initiated closing, only control frames are allowed
208 + if (!is_control) {
209 + websocket_debug(wsc, "Non-control frame rejected in CLOSING_SERVER state");
210 + return WS_ALLOW_DISCARD;
211 + }
212 + return WS_ALLOW_USE;
213 +
214 + case WS_STATE_CLOSING_CLIENT:
215 + // When client initiated closing, we shouldn't process any further frames
216 + // All frames in this state should be silently ignored
217 + websocket_debug(wsc, "Frame rejected in CLOSING_CLIENT state (will be silently ignored)");
218 + return WS_ALLOW_DISCARD;
219 +
220 + case WS_STATE_CLOSED:
221 + // In CLOSED state, no frames should be processed
222 + websocket_debug(wsc, "Frame rejected in CLOSED state");
223 + return WS_ALLOW_DISCARD;
224 +
225 + case WS_STATE_HANDSHAKE:
226 + // In HANDSHAKE state, no WebSocket frames should be processed yet
227 + websocket_debug(wsc, "Frame rejected in HANDSHAKE state");
228 + return WS_ALLOW_ERROR;
229 +
230 + default:
231 + // Unknown state - reject frame
232 + websocket_debug(wsc, "Frame rejected in unknown state: %d", wsc->state);
233 + return WS_ALLOW_DISCARD;
234 + }
235 +}
236 +
237 +// Helper function to handle frame header parsing
238 +bool websocket_protocol_parse_header_from_buffer(const char *buffer, size_t length,
239 + WEBSOCKET_FRAME_HEADER *header) {
240 + if (!buffer || !header || length < 2) {
241 + websocket_debug(NULL, "We need at least 2 bytes to parse a header: buffer=%p, length=%zu", buffer, length);
242 + return false;
243 + }
244 +
245 + // Get first byte - contains FIN bit, RSV bits, and opcode
246 + unsigned char byte1 = (unsigned char)buffer[0];
247 + header->fin = (byte1 & WS_FIN) ? 1 : 0;
248 + header->rsv1 = (byte1 & WS_RSV1) ? 1 : 0;
249 + header->rsv2 = (byte1 & (WS_RSV1 >> 1)) ? 1 : 0;
250 + header->rsv3 = (byte1 & (WS_RSV1 >> 2)) ? 1 : 0;
251 + header->opcode = byte1 & 0x0F;
252 +
253 + // Get second byte - contains MASK bit and initial length
254 + unsigned char byte2 = (unsigned char)buffer[1];
255 + header->mask = (byte2 & WS_MASK) ? 1 : 0;
256 + header->len = byte2 & 0x7F;
257 +
258 + // Calculate header size and payload length based on length field
259 + header->header_size = 2; // Start with 2 bytes for the basic header
260 +
261 + // Determine payload length
262 + if (header->len < 126) {
263 + header->payload_length = header->len;
264 + }
265 + else if (header->len == 126) {
266 + // 16-bit length
267 + if (length < 4) {
268 + websocket_debug(NULL, "We need at least 4 bytes to parse this header: buffer=%p, length=%zu", buffer, length);
269 + return false; // Not enough data
270 + }
271 +
272 + header->payload_length = ((uint64_t)((unsigned char)buffer[2]) << 8) | ((uint64_t)((unsigned char)buffer[3]));
273 + header->header_size += 2;
274 + }
275 + else if (header->len == 127) {
276 + // 64-bit length
277 + if (length < 10) {
278 + websocket_debug(NULL, "We need at least 10 bytes to parse this header: buffer=%p, length=%zu", buffer, length);
279 + return false; // Not enough data
280 + }
281 +
282 + header->payload_length =
283 + ((uint64_t)((unsigned char)buffer[2]) << 56) |
284 + ((uint64_t)((unsigned char)buffer[3]) << 48) |
285 + ((uint64_t)((unsigned char)buffer[4]) << 40) |
286 + ((uint64_t)((unsigned char)buffer[5]) << 32) |
287 + ((uint64_t)((unsigned char)buffer[6]) << 24) |
288 + ((uint64_t)((unsigned char)buffer[7]) << 16) |
289 + ((uint64_t)((unsigned char)buffer[8]) << 8) |
290 + ((uint64_t)((unsigned char)buffer[9]));
291 + header->header_size += 8;
292 + }
293 +
294 + // Read masking key if frame is masked
295 + if (header->mask) {
296 + if (length < header->header_size + 4) return false; // Not enough data
297 +
298 + // Copy masking key
299 + memcpy(header->mask_key, buffer + header->header_size, 4);
300 + header->header_size += 4;
301 + } else {
302 + // Clear mask key if not masked
303 + memset(header->mask_key, 0, 4);
304 + }
305 +
306 + header->payload = (void *)&buffer[header->header_size];
307 + header->frame_size = header->header_size + header->payload_length;
308 +
309 + return true;
310 +}
311 +
312 +// Validate a parsed frame header according to WebSocket protocol rules
313 +static bool websocket_protocol_validate_header(
314 + WS_CLIENT *wsc, WEBSOCKET_FRAME_HEADER *header,
315 + uint64_t payload_length, bool in_fragment_sequence) {
316 + if (!wsc || !header)
317 + return false;
318 +
319 + // Check RSV bits - must be 0 unless extensions are negotiated
320 + if (header->rsv2 || header->rsv3) {
321 + websocket_error(wsc, "Invalid frame: RSV2 or RSV3 bits set");
322 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "RSV2 or RSV3 bits set");
323 + return false;
324 + }
325 +
326 + // RSV1 is only valid if compression is enabled
327 + if (header->rsv1 && (!wsc->compression.enabled)) {
328 + websocket_error(wsc, "Invalid frame: RSV1 bit set but compression not enabled");
329 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "RSV1 bit set without compression");
330 + return false;
331 + }
332 +
333 + // For continuation frames in a compressed message, RSV1 must be 0 per RFC 7692
334 + // Continuation frames for a compressed message must not have RSV1 set
335 + if (header->opcode == WS_OPCODE_CONTINUATION && in_fragment_sequence && header->rsv1) {
336 + websocket_error(wsc, "Invalid frame: Continuation frame should not have RSV1 bit set");
337 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "RSV1 bit set on continuation frame");
338 + return false;
339 + }
340 +
341 + // Check opcode validity
342 + switch (header->opcode) {
343 + case WS_OPCODE_CONTINUATION:
344 + // Continuation frames must be in a fragment sequence
345 + if (!in_fragment_sequence) {
346 + websocket_error(wsc, "Invalid frame: Continuation frame without initial frame");
347 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Continuation frame without initial frame");
348 + return false;
349 + }
350 + break;
351 +
352 + case WS_OPCODE_TEXT:
353 + case WS_OPCODE_BINARY:
354 + // New data frames cannot start inside a fragment sequence
355 + if (in_fragment_sequence) {
356 + websocket_error(wsc, "Invalid frame: New data frame during fragmented message");
357 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "New data frame during fragmented message");
358 + return false;
359 + }
360 + break;
361 +
362 + case WS_OPCODE_CLOSE:
363 + case WS_OPCODE_PING:
364 + case WS_OPCODE_PONG:
365 + // Control frames must not be fragmented
366 + if (!header->fin) {
367 + websocket_error(wsc, "Invalid frame: Fragmented control frame");
368 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Fragmented control frame");
369 + return false;
370 + }
371 +
372 + // Control frames must have payload ≤ 125 bytes
373 + if (payload_length > 125) {
374 + websocket_error(wsc, "Invalid frame: Control frame payload too large (%llu bytes)",
375 + (unsigned long long)payload_length);
376 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Control frame payload too large");
377 + return false;
378 + }
379 + break;
380 +
381 + default:
382 + // Unknown opcode
383 + websocket_error(wsc, "Invalid frame: Unknown opcode: 0x%x", (unsigned int)header->opcode);
384 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Unknown opcode");
385 + return false;
386 + }
387 +
388 + // Validate payload length against limits
389 + if (payload_length > (uint64_t)WS_MAX_FRAME_LENGTH) {
390 + websocket_error(wsc, "Invalid frame: Payload too large (%llu bytes)",
391 + (unsigned long long)payload_length);
392 + websocket_protocol_exception(wsc, WS_CLOSE_MESSAGE_TOO_BIG, "Frame payload too large");
393 + return false;
394 + }
395 +
396 + // All checks passed
397 + return true;
398 +}
399 +
400 +// Process a control frame directly without creating a message structure
401 +static bool websocket_protocol_process_control_message(
402 + WS_CLIENT *wsc, WEBSOCKET_OPCODE opcode,
403 + char *payload, size_t payload_length,
404 + bool is_masked, const unsigned char *mask_key) {
405 + websocket_debug(wsc, "Processing control frame opcode=0x%x, payload_length=%zu, is_masked=%d, connection state=%d",
406 + opcode, payload_length, is_masked, wsc->state);
407 +
408 + // If payload is masked, unmask it first
409 + if (is_masked && mask_key && payload && payload_length > 0)
410 + websocket_unmask(payload, payload, payload_length, mask_key);
411 +
412 + switch (opcode) {
413 + case WS_OPCODE_CLOSE: {
414 + worker_is_busy(WORKERS_WEBSOCKET_MSG_CLOSE);
415 +
416 + uint16_t code = WS_CLOSE_NORMAL;
417 + char reason[124];
418 + reason[0] = '\0'; // Initialize reason string
419 +
420 + // Check for malformed CLOSE frame payload
421 + if (payload && payload_length == 1) {
422 + websocket_error(wsc, "Invalid CLOSE frame payload length: 1 byte (must be 0 or >= 2 bytes)");
423 +
424 + // This is a protocol violation - handle it consistently through the protocol exception mechanism
425 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Invalid payload length");
426 +
427 + // Return true since we handled the message
428 + return true;
429 + }
430 + // Parse close code if present
431 + else if (payload && payload_length >= 2) {
432 + code = ((uint16_t)((unsigned char)payload[0]) << 8) |
433 + ((uint16_t)((unsigned char)payload[1]));
434 +
435 + // Validate close code
436 + if (!websocket_validate_close_code(code)) {
437 + websocket_error(wsc, "Invalid close code: %u", code);
438 +
439 + // This is a protocol violation - handle it through the protocol exception mechanism
440 + // This will send a close frame with 1002 (protocol error) and set the appropriate state
441 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Invalid close code");
442 +
443 + // Return true since we handled the message
444 + return true;
445 + }
446 + // Check UTF-8 validity of the reason text
447 + else if (payload_length > 2) {
448 + // RFC 6455 requires all control frame payloads (including close reasons) to be valid UTF-8
449 + if (!websocket_validate_utf8(payload + 2, payload_length - 2)) {
450 + websocket_error(wsc, "Invalid UTF-8 in close frame reason");
451 + code = WS_CLOSE_INVALID_PAYLOAD; // 1007 - Invalid frame payload data
452 + strncpyz(reason, "Invalid UTF-8 in close reason", sizeof(reason) - 1);
453 + }
454 + else {
455 + // Valid UTF-8, copy the reason
456 + strncpyz(reason, payload + 2, MIN(payload_length - 2, sizeof(reason) - 1));
457 + }
458 + }
459 + }
460 +
461 + // Different handling based on connection state
462 + if (wsc->state == WS_STATE_OPEN) {
463 + // This is the initial CLOSE from client - respond with our own CLOSE
464 + websocket_debug(wsc, "Received initial CLOSE frame from client, responding with CLOSE");
465 +
466 + // Send close frame in response
467 + websocket_protocol_send_close(wsc, code, reason);
468 +
469 + wsc->state = WS_STATE_CLOSING_CLIENT;
470 + wsc->flush_and_remove_client = true;
471 +
472 + // IMPORTANT: do not call websocket_write_data() here
473 + // because it prevents wsc->flush_and_remove_client from removing the client!
474 + }
475 + else if (wsc->state == WS_STATE_CLOSING_SERVER) {
476 + // We initiated the closing handshake and now received client's response
477 + // This completes the closing handshake
478 + websocket_debug(wsc, "Closing handshake complete - received client's CLOSE response to our close");
479 +
480 + // Ensure immediate removal from thread/poll
481 + if (wsc->wth) {
482 + websocket_info(wsc, "Closing TCP connection after completed handshake (server initiated)");
483 + websocket_thread_send_command(wsc->wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
484 + }
485 +
486 + // The RFC requires us to close the TCP connection immediately now
487 + wsc->state = WS_STATE_CLOSED;
488 + }
489 + else if (wsc->state == WS_STATE_CLOSING_CLIENT) {
490 + // Client already sent a CLOSE, and we responded, but they sent another CLOSE
491 + // This is not strictly according to protocol, but we'll handle it gracefully
492 + websocket_debug(wsc, "Received another CLOSE frame while in client-initiated closing state");
493 +
494 + // Remove client from thread
495 + if (wsc->wth) {
496 + websocket_info(wsc, "Closing TCP connection (duplicate close from client)");
497 + websocket_thread_send_command(wsc->wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
498 + }
499 +
500 + // Move to CLOSED state and close the connection
501 + wsc->state = WS_STATE_CLOSED;
502 + }
503 + else {
504 + // Already in CLOSED state - ignore
505 + websocket_debug(wsc, "Ignoring CLOSE frame - connection already in CLOSED state");
506 + }
507 + return true;
508 + }
509 +
510 + case WS_OPCODE_PING:
511 + worker_is_busy(WORKERS_WEBSOCKET_MSG_PING);
512 +
513 + // If we're in CLOSING or CLOSED state, decide how to handle PING based on state
514 + if (wsc->state == WS_STATE_CLOSING_SERVER || wsc->state == WS_STATE_CLOSING_CLIENT || wsc->state == WS_STATE_CLOSED) {
515 + // When we initiated closing, we should still respond to control frames
516 + if (wsc->state == WS_STATE_CLOSING_SERVER) {
517 + websocket_debug(wsc, "Received PING during server-initiated closing, responding with PONG");
518 + return websocket_protocol_send_pong(wsc, payload, payload_length) > 0;
519 + }
520 +
521 + // For client-initiated closing or CLOSED, we should ignore control frames
522 + websocket_debug(wsc, "Ignoring PING frame - connection in %s state",
523 + wsc->state == WS_STATE_CLOSING_CLIENT ? "client closing" : "closed");
524 + return true; // Successfully processed (by ignoring)
525 + }
526 +
527 + // Ping frame - respond with pong
528 + websocket_debug(wsc, "Received PING frame with %zu bytes, responding with PONG", payload_length);
529 +
530 + // Send pong with the same payload
531 + return websocket_protocol_send_pong(wsc, payload, payload_length) > 0;
532 +
533 + case WS_OPCODE_PONG:
534 + worker_is_busy(WORKERS_WEBSOCKET_MSG_PONG);
535 +
536 + // If we're in CLOSING or CLOSED state, decide how to handle PONG
537 + if (wsc->state == WS_STATE_CLOSING_SERVER || wsc->state == WS_STATE_CLOSING_CLIENT || wsc->state == WS_STATE_CLOSED) {
538 + // We can safely ignore PONG frames in any closing or closed state
539 + websocket_debug(wsc, "Ignoring PONG frame - connection in %s state",
540 + wsc->state == WS_STATE_CLOSING_SERVER ? "server closing" :
541 + wsc->state == WS_STATE_CLOSING_CLIENT ? "client closing" : "closed");
542 + return true; // Successfully processed (by ignoring)
543 + }
544 +
545 + // Pong frame - update last activity time
546 + websocket_debug(wsc, "Received PONG frame, updating last activity time");
547 + wsc->last_activity_t = now_monotonic_sec();
548 + return true;
549 +
550 + default:
551 + worker_is_busy(WORKERS_WEBSOCKET_MSG_INVALID);
552 + break;
553 + }
554 +
555 + websocket_error(wsc, "Unknown control opcode: %d", opcode);
556 + return false;
557 +}
558 +
559 +// Parse a frame from a buffer and append it to the current message if applicable.
560 +// Returns one of the following:
561 +// - WS_FRAME_ERROR: An error occurred, connection should be closed
562 +// - WS_FRAME_NEED_MORE_DATA: More data is needed to complete the frame
563 +// - WS_FRAME_COMPLETE: Frame was successfully parsed and handled, but is not a complete message yet
564 +// - WS_FRAME_MESSAGE_READY: Message is ready for processing
565 +static WEBSOCKET_FRAME_RESULT
566 +websocket_protocol_consume_frame(WS_CLIENT *wsc, char *data, size_t length, ssize_t *bytes_processed) {
567 + if (!wsc || !data || !length || !bytes_processed)
568 + return WS_FRAME_ERROR;
569 +
570 + size_t bytes = *bytes_processed = 0;
571 +
572 + // Local variables for frame processing
573 + WEBSOCKET_FRAME_HEADER header = { 0 };
574 +
575 + // Step 1: Parse the frame header
576 + if (!websocket_protocol_parse_header_from_buffer(data, length, &header)) {
577 + websocket_debug(wsc, "Not enough data to parse a complete header: bytes available = %zu",
578 + length);
579 + return WS_FRAME_NEED_MORE_DATA;
580 + }
581 +
582 + if(header.frame_size > wsc->max_message_size)
583 + wsc->max_message_size = header.frame_size;
584 +
585 + // Check if we have enough data for the complete frame (header + payload)
586 + // If not, don't consume any bytes and wait for more data
587 + if (bytes + header.frame_size > length) {
588 +
589 + // let the circular buffer know how much data we need
590 + wsc->next_frame_size = header.frame_size;
591 +
592 + worker_is_busy(WORKERS_WEBSOCKET_INCOMPLETE_FRAME);
593 + websocket_debug(wsc,
594 + "RX FRAME INCOMPLETE (need %zu bytes more): OPCODE=0x%x, FIN=%s, RSV1=%d, RSV2=%d, RSV3=%d, MASK=%s, LEN=%d, "
595 + "PAYLOAD_LEN=%zu, HEADER_SIZE=%zu, FRAME_SIZE=%zu, MASK=%02x%02x%02x%02x, bytes available = %zu",
596 + (bytes + header.frame_size) - length,
597 + header.opcode, header.fin ? "True" : "False", header.rsv1, header.rsv2, header.rsv3,
598 + header.mask ? "True" : "False", header.len,
599 + header.payload_length, header.header_size, header.frame_size,
600 + header.mask_key[0], header.mask_key[1], header.mask_key[2], header.mask_key[3],
601 + length);
602 +
603 + return WS_FRAME_NEED_MORE_DATA;
604 + }
605 + wsc->next_frame_size = 0; // reset it, since we have enough data now
606 +
607 + worker_is_busy(WORKERS_WEBSOCKET_COMPLETE_FRAME);
608 +
609 + // Log detailed header information for debugging
610 + websocket_debug(wsc,
611 + "RX FRAME: OPCODE=0x%x, FIN=%s, RSV1=%d, RSV2=%d, RSV3=%d, MASK=%s, LEN=%d, "
612 + "PAYLOAD_LEN=%zu, HEADER_SIZE=%zu, FRAME_SIZE=%zu, MASK=%02x%02x%02x%02x",
613 + header.opcode, header.fin ? "True" : "False", header.rsv1, header.rsv2, header.rsv3,
614 + header.mask ? "True" : "False", header.len,
615 + header.payload_length, header.header_size, header.frame_size,
616 + header.mask_key[0], header.mask_key[1], header.mask_key[2], header.mask_key[3]);
617 +
618 + // Check for invalid RSV bits
619 + if (header.rsv2 || header.rsv3 || (header.rsv1 && !wsc->compression.enabled)) {
620 + const char *reason = header.rsv2 ? "RSV2 bit set" :
621 + (header.rsv3 ? "RSV3 bit set" : "RSV1 bit set without compression");
622 +
623 + // Handle protocol exception in a centralized way
624 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, reason);
625 + return WS_FRAME_ERROR;
626 + }
627 +
628 + // Check if this frame is allowed in the current connection state
629 + switch(websocket_is_frame_allowed(wsc, &header)) {
630 + case WS_ALLOW_USE:
631 + break;
632 +
633 + case WS_ALLOW_DISCARD:
634 + // we have already logged in websocket_is_frame_allowed()
635 + return WS_FRAME_COMPLETE;
636 +
637 + default:
638 + case WS_ALLOW_ERROR: {
639 + char reason[128];
640 +
641 + snprintf(reason, sizeof(reason), "Frame not allowed in %s state",
642 + wsc->state == WS_STATE_CLOSING_SERVER ? "server closing" :
643 + wsc->state == WS_STATE_CLOSING_CLIENT ? "client closing" :
644 + "current");
645 +
646 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, reason);
647 + return WS_FRAME_ERROR;
648 + }
649 + }
650 +
651 + // Step 2: Validate the frame header
652 + if (!websocket_protocol_validate_header(wsc, &header, header.payload_length, !wsc->message_complete)) {
653 + // Invalid frame - websocket_protocol_validate_header sent a close frame
654 + // but we should handle the connection closing consistently
655 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Invalid frame header");
656 + return WS_FRAME_ERROR;
657 + }
658 +
659 + // Advance past the header
660 + bytes += header.header_size;
661 +
662 + if (websocket_frame_is_control_opcode(header.opcode)) {
663 + // Handle control frames (PING, PONG, CLOSE) directly
664 + websocket_debug(wsc, "Handling control frame: opcode=0x%x, payload_length=%zu",
665 + (unsigned)header.opcode, header.payload_length);
666 +
667 + // Process the control frame with optional payload
668 + char *payload = (header.payload_length > 0) ? (data + bytes) : NULL;
669 +
670 + // Process control message directly without creating a message object
671 + if (!websocket_protocol_process_control_message(
672 + wsc, (WEBSOCKET_OPCODE)header.opcode,
673 + payload, header.payload_length,
674 + header.mask ? true : false,
675 + header.mask_key)) {
676 + websocket_error(wsc, "Failed to process control frame");
677 + return WS_FRAME_ERROR;
678 + }
679 +
680 + // Update bytes processed
681 + if (header.payload_length > 0)
682 + bytes += (size_t)header.payload_length;
683 +
684 + *bytes_processed = bytes;
685 +
686 + return WS_FRAME_COMPLETE; // Return COMPLETE so we continue processing other frames
687 + }
688 +
689 + // For non-control frames (text, binary, continuation), check if connection is closing
690 + if (wsc->state == WS_STATE_CLOSING_SERVER || wsc->state == WS_STATE_CLOSING_CLIENT || wsc->state == WS_STATE_CLOSED) {
691 + // Per RFC 6455, once the closing handshake is started, we should ignore non-control frames
692 + websocket_debug(wsc, "Ignoring non-control frame (opcode=0x%x) - connection in %s state",
693 + header.opcode,
694 + wsc->state == WS_STATE_CLOSING_SERVER ? "server closing" :
695 + wsc->state == WS_STATE_CLOSING_CLIENT ? "client closing" : "closed");
696 +
697 + // Consume the frame bytes but don't process it
698 + bytes += header.header_size + header.payload_length;
699 + *bytes_processed = bytes;
700 +
701 + return WS_FRAME_COMPLETE;
702 + }
703 +
704 + // Step 3: Handle the frame based on its opcode
705 + if (header.opcode == WS_OPCODE_CONTINUATION) {
706 + // This is a continuation frame - need an existing message in progress
707 + if (wsc->message_complete) {
708 + websocket_error(wsc, "Received continuation frame with no message in progress");
709 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "Continuation frame without initial frame");
710 + return WS_FRAME_ERROR;
711 + }
712 +
713 + // If it's a zero-length frame, we don't need to append any data
714 + if (header.payload_length == 0) {
715 + // For zero-length non-final frames, just update and continue
716 + if (!header.fin) {
717 + // Non-final zero-length frame
718 + websocket_debug(wsc, "Zero-length non-final continuation frame");
719 + *bytes_processed = bytes;
720 + wsc->frame_id++;
721 + return WS_FRAME_COMPLETE;
722 + }
723 +
724 + // Final zero-length frame - mark message as complete
725 + wsc->message_complete = true;
726 + *bytes_processed = bytes;
727 + return WS_FRAME_MESSAGE_READY;
728 + }
729 +
730 + // The message buffer length is updated as we append frame data
731 + }
732 + else {
733 + if(!header.payload_length) {
734 + websocket_debug(wsc, "Received data frame with zero-length payload (fin=%d)", header.fin);
735 +
736 + // Initialize the client's message state for a new message
737 + websocket_client_message_reset(wsc);
738 + wsc->opcode = (WEBSOCKET_OPCODE)header.opcode;
739 + wsc->is_compressed = header.rsv1 ? true : false;
740 +
741 + // The most important part: for fragmented messages (non-final frames),
742 + // we must set message_complete to false, regardless of payload length
743 + wsc->message_complete = header.fin;
744 +
745 + // Buffer length is already 0 after reset
746 + wsc->frame_id = 0;
747 +
748 + // Check if this is a final frame
749 + if (header.fin) {
750 + // Final frame - message is already marked as complete
751 + *bytes_processed = bytes;
752 + return WS_FRAME_MESSAGE_READY;
753 + } else {
754 + // Non-final frame - continue to next frame
755 + *bytes_processed = bytes;
756 + wsc->frame_id++;
757 + return WS_FRAME_COMPLETE;
758 + }
759 + }
760 +
761 + // This is a new data frame (TEXT or BINARY)
762 + // If we have an existing message in progress, it's an error
763 + if (!wsc->message_complete) {
764 + websocket_error(wsc, "Received new data frame while another message is in progress");
765 + websocket_protocol_exception(wsc, WS_CLOSE_PROTOCOL_ERROR, "New data frame during fragmented message");
766 + return WS_FRAME_ERROR;
767 + }
768 +
769 + // Initialize the client's message state for a new message
770 + websocket_client_message_reset(wsc);
771 + wsc->opcode = (WEBSOCKET_OPCODE)header.opcode;
772 + wsc->is_compressed = header.rsv1 ? true : false;
773 +
774 + // For fragmented messages (non-final frames), we must set message_complete to false
775 + // This needs to be consistently done for both empty and non-empty frames
776 + wsc->message_complete = header.fin;
777 +
778 + // Buffer length will be updated when we append the payload data
779 + wsc->frame_id = 0;
780 + }
781 +
782 + // Step 4: Append payload data to the message
783 +
784 + if (header.payload_length > 0) {
785 + char *src = header.payload;
786 +
787 + if (header.mask) {
788 + // Payload is masked - need to unmask it first
789 + websocket_debug(wsc, "Unmasking and appending payload data at position %zu (key=%02x%02x%02x%02x)",
790 + wsb_length(&wsc->payload),
791 + header.mask_key[0], header.mask_key[1], header.mask_key[2], header.mask_key[3]);
792 +
793 + // Use the new helper function to unmask and append the data in one step
794 + wsb_unmask_and_append(&wsc->payload, src, header.payload_length, header.mask_key);
795 + }
796 + else {
797 + // Payload is not masked - can directly append
798 + websocket_debug(wsc, "Appending unmasked payload data at position %zu", wsb_length(&wsc->payload));
799 +
800 + // Append unmasked data directly
801 + wsb_append(&wsc->payload, src, header.payload_length);
802 + }
803 +
804 + // Dump payload for debugging
805 + size_t buffer_length = wsb_length(&wsc->payload);
806 + websocket_dump_debug(wsc,
807 + wsb_data(&wsc->payload) + (buffer_length - header.payload_length),
808 + header.payload_length,
809 + "RX FRAME PAYLOAD");
810 + }
811 +
812 + // Step 5: At this point, we know we've processed a complete frame
813 + wsc->frame_id++;
814 +
815 + bytes += header.payload_length;
816 + *bytes_processed = bytes;
817 +
818 + // If this is a final frame, mark the message as complete
819 + if (header.fin)
820 + return WS_FRAME_MESSAGE_READY;
821 +
822 + // Non-final frame, message is incomplete - move to next frame
823 + return WS_FRAME_COMPLETE;
824 +}
825 +
826 +// Process incoming data from the WebSocket client
827 +// This function's job is to:
828 +// 1. Consume frames from the input buffer
829 +// 2. Build messages
830 +// 3. Process complete messages
831 +ssize_t websocket_protocol_got_data(WS_CLIENT *wsc, char *data, size_t length) {
832 + if (!wsc || !data || !length)
833 + return -1;
834 +
835 + // Keep processing frames until we can't process any more
836 + size_t processed = 0;
837 + while (processed < length) {
838 + // Try to consume one complete frame
839 + ssize_t consumed = 0;
840 + WEBSOCKET_FRAME_RESULT result = websocket_protocol_consume_frame(wsc, data + processed, length - processed, &consumed);
841 +
842 + websocket_debug(wsc, "Frame processing result: %d, processed: %zu/%zu", result, consumed, length);
843 +
844 + // Safety check to ensure we always move forward in the buffer
845 + if (consumed == 0 && result != WS_FRAME_NEED_MORE_DATA && result != WS_FRAME_ERROR) {
846 + // If we're processing a frame but not consuming bytes, we might be stuck
847 + websocket_error(wsc, "Protocol processing stalled - consumed 0 bytes but not waiting for more data (%d)", (int)result);
848 + return -(ssize_t)processed;
849 + }
850 +
851 + switch (result) {
852 + case WS_FRAME_ERROR:
853 + // Error occurred during frame processing
854 + websocket_error(wsc, "Error processing WebSocket frame");
855 + return processed ? -(ssize_t)processed : -1;
856 +
857 + case WS_FRAME_NEED_MORE_DATA:
858 + // Need more data to complete the current frame
859 + websocket_debug(wsc, "Need more data to complete the current frame");
860 + return (ssize_t)processed;
861 +
862 + case WS_FRAME_COMPLETE:
863 + // Frame was processed successfully, but more frames are needed for a complete message
864 + websocket_debug(wsc, "Frame complete, but message not yet complete");
865 + processed += consumed;
866 + continue;
867 +
868 + case WS_FRAME_MESSAGE_READY:
869 + worker_is_busy(WORKERS_WEBSOCKET_MESSAGE);
870 + processed += consumed;
871 +
872 + wsc->message_complete = true;
873 + if (!websocket_client_process_message(wsc))
874 + websocket_error(wsc, "Failed to process completed message");
875 +
876 + continue;
877 + }
878 + }
879 +
880 + return (ssize_t)processed;
881 +}
882 +
src/web/websocket/websocket-send.c new
+411
@@ -0,0 +1,411 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-internal.h"
4 +
5 +// --------------------------------------------------------------------------------------------------------------------
6 +// writing to the socket
7 +
8 +// Actually write data to the client socket
9 +ssize_t websocket_write_data(WS_CLIENT *wsc) {
10 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
11 +
12 + worker_is_busy(WORKERS_WEBSOCKET_SOCK_SEND);
13 +
14 + if (!wsc->out_buffer.data || wsc->sock.fd < 0)
15 + return -1;
16 +
17 + ssize_t bytes_written = 0;
18 +
19 + // Let cbuffer_next_unsafe determine if there's data to write
20 + // This correctly handles the circular buffer wrap-around cases
21 +
22 + // Get data to write from circular buffer
23 + char *data;
24 + size_t data_length = cbuffer_next_unsafe(&wsc->out_buffer, &data);
25 + if (data_length == 0)
26 + goto done;
27 +
28 + // Dump the data being written for debugging
29 + websocket_dump_debug(wsc, data, data_length, "TX SOCK %zu bytes", data_length);
30 +
31 + // In the websocket thread we want non-blocking behavior
32 + // Use nd_sock_write with a single retry
33 + bytes_written = nd_sock_write(&wsc->sock, data, data_length, 1); // 1 retry for non-blocking write
34 +
35 + if (bytes_written < 0) {
36 + websocket_error(wsc, "Failed to write to client: %s", strerror(errno));
37 + goto done;
38 + }
39 +
40 + // Remove written bytes from circular buffer
41 + if (bytes_written > 0)
42 + cbuffer_remove_unsafe(&wsc->out_buffer, bytes_written);
43 +
44 +done:
45 + websocket_thread_update_client_poll_flags(wsc);
46 + return bytes_written;
47 +}
48 +
49 +// --------------------------------------------------------------------------------------------------------------------
50 +
51 +static inline size_t select_header_size(size_t payload_len) {
52 + if (payload_len < 126)
53 + return 2;
54 + else if (payload_len <= 65535)
55 + return 4;
56 + else
57 + return 10;
58 +}
59 +
60 +// Create and send a WebSocket frame
61 +int websocket_protocol_send_frame(
62 + WS_CLIENT *wsc, const char *payload, size_t payload_len,
63 + WEBSOCKET_OPCODE opcode, bool use_compression) {
64 +
65 + if(!wsc)
66 + return -1;
67 +
68 + const char *disconnect_msg = "";
69 + z_stream *zstrm = wsc->compression.deflate_stream;
70 +
71 + if (wsc->sock.fd < 0) {
72 + disconnect_msg = "Client not connected";
73 + goto abnormal_disconnect;
74 + }
75 +
76 + // Validate parameters based on WebSocket protocol
77 + if (websocket_frame_is_control_opcode(opcode) && payload_len > 125) {
78 + disconnect_msg = "Control frame payload too large";
79 + goto abnormal_disconnect;
80 + }
81 +
82 + // Check if we should actually use compression
83 + bool compress = !websocket_frame_is_control_opcode(opcode) &&
84 + use_compression &&
85 + payload && payload_len &&
86 + wsc->compression.enabled &&
87 + zstrm &&
88 + payload_len >= WS_COMPRESS_MIN_SIZE;
89 +
90 + // Calculate maximum possible compressed size using deflateBound
91 + size_t max_compressed_size = payload_len;
92 + if (compress) {
93 + // Use deflateBound to accurately calculate maximum possible size
94 + max_compressed_size = deflateBound(zstrm, payload_len);
95 +
96 + // Add 4 bytes for Z_SYNC_FLUSH trailer (will be removed later)
97 + max_compressed_size += 4;
98 +
99 + // Ensure the destination can fit the uncompressed data too
100 + max_compressed_size = MAX(payload_len, max_compressed_size);
101 + }
102 +
103 + // Determine header size based on maximum potential size
104 + size_t header_size = select_header_size(max_compressed_size);
105 +
106 + // Calculate maximum potential frame size
107 + size_t max_frame_size = header_size + max_compressed_size;
108 +
109 + // Reserve space in the circular buffer for the entire frame
110 + unsigned char *header_dst = (unsigned char *)cbuffer_reserve_unsafe(&wsc->out_buffer, max_frame_size);
111 + if (!header_dst) {
112 + disconnect_msg = "Buffer full - too much outgoing data";
113 + goto abnormal_disconnect;
114 + }
115 +
116 + // The payload will be written directly after the header in our reserved buffer
117 + char *payload_dst = (char *)(header_dst + header_size);
118 + size_t final_payload_len = 0;
119 +
120 + if (compress) {
121 + // Setup temporary parameters for the compressor to use
122 + // We'll use the space after our header for the compressed data
123 + zstrm->next_in = (Bytef *)payload;
124 + zstrm->avail_in = payload_len;
125 + zstrm->next_out = (Bytef *)payload_dst;
126 + zstrm->avail_out = max_compressed_size;
127 + zstrm->total_in = 0;
128 + zstrm->total_out = 0;
129 +
130 + // Compress with sync flush to ensure data is flushed
131 + int ret = deflate(zstrm, Z_SYNC_FLUSH);
132 +
133 + bool success = false;
134 + if (ret == Z_STREAM_END || (ret == Z_OK && zstrm->avail_in == 0 && zstrm->avail_out > 0))
135 + success = true;
136 + else if (ret == Z_OK && zstrm->avail_in == 0 && zstrm->avail_out == 0) {
137 + unsigned pending = Z_NULL;
138 + int bits = Z_NULL;
139 + if(deflatePending(zstrm, &pending, &bits) == Z_OK &&
140 + (pending == Z_NULL && bits == Z_NULL))
141 + success = true;
142 + }
143 +
144 + uInt avail_in = zstrm->avail_in;
145 + uInt avail_out = zstrm->avail_out;
146 + uLong total_in = zstrm->total_in;
147 + uLong total_out = zstrm->total_out;
148 +
149 + // we are done - reset the stream to avoid heap-use-after-free issues later
150 + if (deflateReset(zstrm) != Z_OK) {
151 + disconnect_msg = "Deflate reset failed";
152 + goto abnormal_disconnect;
153 + }
154 +
155 + // Calculate compressed size if successful
156 + if (!success || total_out <= 4) {
157 + // Compression failed
158 + websocket_error(wsc, "Compression failed: %s "
159 + "(ret = %d, avail_in = %u, avail_out = %u, total_in = %lu, total_out = %lu) - "
160 + "sending uncompressed payload",
161 + zError(ret), ret, avail_in, avail_out, total_in, total_out);
162 + compress = false;
163 + }
164 + else {
165 + // As per RFC 7692, remove trailing 4 bytes (00 00 FF FF) from Z_SYNC_FLUSH
166 + final_payload_len = max_compressed_size - avail_out - 4;
167 +
168 + websocket_debug(wsc, "Compressed payload from %zu to %zu bytes (%.1f%%)",
169 + payload_len, final_payload_len,
170 + (double)final_payload_len * 100.0 / (double)payload_len);
171 +
172 + // we may have selected a bigger header size than needed
173 + // so we need to move the payload to the right place
174 + size_t optimal_header_size = select_header_size(final_payload_len);
175 + if(optimal_header_size < header_size) {
176 + char *dst = (char *)header_dst + optimal_header_size;
177 + char *src = payload_dst;
178 + memmove(dst, src, final_payload_len);
179 + payload_dst = dst;
180 + header_size = optimal_header_size;
181 + }
182 + }
183 +
184 + // ensure all pointer values are NULL, so that there is no trace back to this compression
185 + zstrm->next_in = NULL;
186 + zstrm->avail_in = 0;
187 + zstrm->next_out = NULL;
188 + zstrm->avail_out = 0;
189 + zstrm->total_in = 0;
190 + zstrm->total_out = 0;
191 + }
192 +
193 + if(!compress && payload && payload_len > 0) {
194 + memcpy(payload_dst, payload, payload_len);
195 + final_payload_len = payload_len;
196 + }
197 +
198 + // Write the header
199 + // First byte: FIN(1) + RSV1(compress) + RSV2(0) + RSV3(0) + OPCODE(4)
200 + header_dst[0] = 0x80 | (compress ? 0x40 : 0) | (opcode & 0x0F);
201 +
202 + // Write payload length with the appropriate format
203 + switch(header_size) {
204 + case 2:
205 + header_dst[1] = final_payload_len & 0x7F;
206 + break;
207 +
208 + case 4:
209 + header_dst[1] = 126;
210 + header_dst[2] = (final_payload_len >> 8) & 0xFF;
211 + header_dst[3] = final_payload_len & 0xFF;
212 + break;
213 +
214 + case 10:
215 + header_dst[1] = 127;
216 + header_dst[2] = (final_payload_len >> 56) & 0xFF;
217 + header_dst[3] = (final_payload_len >> 48) & 0xFF;
218 + header_dst[4] = (final_payload_len >> 40) & 0xFF;
219 + header_dst[5] = (final_payload_len >> 32) & 0xFF;
220 + header_dst[6] = (final_payload_len >> 24) & 0xFF;
221 + header_dst[7] = (final_payload_len >> 16) & 0xFF;
222 + header_dst[8] = (final_payload_len >> 8) & 0xFF;
223 + header_dst[9] = final_payload_len & 0xFF;
224 + break;
225 +
226 + default:
227 + // impossible case - added to avoid compiler warning
228 + disconnect_msg = "Invalid header size";
229 + goto abnormal_disconnect;
230 + }
231 +
232 + // Commit the final frame size (header + payload)
233 + size_t final_frame_size = header_size + final_payload_len;
234 + cbuffer_commit_reserved_unsafe(&wsc->out_buffer, final_frame_size);
235 +
236 +#ifdef NETDATA_INTERNAL_CHECKS
237 + // Log frame being sent with detailed format matching the received frame logging
238 + WEBSOCKET_FRAME_HEADER header;
239 + if(!websocket_protocol_parse_header_from_buffer((const char *)header_dst, header_size, &header)) {
240 + disconnect_msg = "Failed to parse the header we generated";
241 + goto abnormal_disconnect;
242 + }
243 +
244 + websocket_debug(wsc,
245 + "TX FRAME: OPCODE=0x%x, FIN=%s, RSV1=%d, RSV2=%d, RSV3=%d, MASK=%s, LEN=%d, "
246 + "PAYLOAD_LEN=%zu, HEADER_SIZE=%zu, FRAME_SIZE=%zu, MASK=%02x%02x%02x%02x",
247 + header.opcode, header.fin ? "True" : "False", header.rsv1, header.rsv2, header.rsv3,
248 + header.mask ? "True" : "False", header.len,
249 + header.payload_length, header.header_size, header.frame_size,
250 + header.mask_key[0], header.mask_key[1], header.mask_key[2], header.mask_key[3]);
251 +#endif
252 +
253 + // Make sure the client's poll flags include WRITE
254 + websocket_thread_update_client_poll_flags(wsc);
255 +
256 + return (int)final_frame_size;
257 +
258 +abnormal_disconnect:
259 + websocket_error(wsc, "triggering abnormal disconnect: %s", disconnect_msg);
260 + websocket_thread_send_command(wsc->wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
261 + return -1;
262 +
263 +//graceful_disconnect:
264 +// // the current implementation does not support graceful disconnect - so we do an abnormal one
265 +// websocket_error(wsc, "triggering graceful disconnect: %s", disconnect_msg);
266 +// websocket_thread_send_command(wsc->wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
267 +// return -1;
268 +}
269 +
270 +// Send a text message
271 +int websocket_protocol_send_text(WS_CLIENT *wsc, const char *text) {
272 + if (!wsc)
273 + return -1;
274 +
275 + // Special handling for null or empty text message
276 + if (!text || text[0] == '\0') {
277 + websocket_debug(wsc, "Sending empty text message");
278 +
279 + // Use an empty buffer for zero-length text messages
280 + static const char empty_data[1] = {0};
281 + return websocket_protocol_send_frame(wsc, empty_data, 0, WS_OPCODE_TEXT, false);
282 + }
283 +
284 + size_t text_len = strlen(text);
285 +
286 + websocket_debug(wsc, "Sending text message, length=%zu", text_len);
287 +
288 + // Dump text message for debugging
289 + websocket_dump_debug(wsc, text, text_len, "TX TEXT MSG");
290 +
291 + // Enable compression for text messages by default
292 + return websocket_protocol_send_frame(wsc, text, text_len, WS_OPCODE_TEXT, true);
293 +}
294 +
295 +// Send a binary message
296 +int websocket_protocol_send_binary(WS_CLIENT *wsc, const void *data, size_t length) {
297 + if (!wsc)
298 + return -1;
299 +
300 + // Special handling for empty binary message
301 + if (!data || length == 0) {
302 + websocket_debug(wsc, "Sending empty binary message");
303 +
304 + // Use an empty buffer for zero-length binary messages
305 + static const char empty_data[1] = {0};
306 + return websocket_protocol_send_frame(wsc, empty_data, 0, WS_OPCODE_BINARY, false);
307 + }
308 +
309 + websocket_debug(wsc, "Sending binary message, length=%zu", length);
310 +
311 + // Dump binary message for debugging
312 + websocket_dump_debug(wsc, data, length, "TX BIN MSG");
313 +
314 + return websocket_protocol_send_frame(wsc, data, length, WS_OPCODE_BINARY, true);
315 +}
316 +
317 +// Send a close frame
318 +int websocket_protocol_send_close(WS_CLIENT *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason) {
319 + if (!wsc || wsc->sock.fd < 0)
320 + return -1;
321 +
322 + // Only send a close frame if we're in a valid state to do so
323 + // Per RFC 6455: An endpoint MUST NOT send any more data frames after sending a Close frame
324 + // CLOSING_CLIENT means we already responded to a client's close and shouldn't send another
325 + if (wsc->state == WS_STATE_CLOSED ||
326 + wsc->state == WS_STATE_CLOSING_SERVER ||
327 + wsc->state == WS_STATE_CLOSING_CLIENT)
328 + return -1;
329 +
330 + // Validate close code
331 + if (!websocket_validate_close_code((uint16_t)code)) {
332 + websocket_error(wsc, "Invalid close code: %d (%s)", code, WEBSOCKET_CLOSE_CODE_2str(code));
333 + code = WS_CLOSE_PROTOCOL_ERROR;
334 + reason = "Invalid close code";
335 + }
336 +
337 + // Prepare close payload: 2-byte code + optional reason text
338 + size_t reason_len = reason ? strlen(reason) : 0;
339 +
340 + // Control frames max size is 125 bytes
341 + if (reason_len > 123) {
342 + websocket_error(wsc, "Close frame payload too large: %zu bytes (max 123)", reason_len);
343 + reason_len = 123; // Truncate reason to fit
344 + }
345 +
346 + // Use stack buffer for close frame payload (max 125 bytes per RFC 6455)
347 + size_t payload_len = 2 + reason_len;
348 + char payload[payload_len];
349 +
350 + // Set status code in network byte order (big-endian)
351 + uint16_t code_value = (uint16_t)code;
352 + payload[0] = (code_value >> 8) & 0xFF;
353 + payload[1] = code & 0xFF;
354 +
355 + // Add reason if provided (truncate if necessary)
356 + if (reason && reason_len > 0)
357 + memcpy(payload + 2, reason, reason_len);
358 +
359 + // Call the close handler if registered - this is used to inject a message on close if needed
360 + if(wsc->on_close)
361 + wsc->on_close(wsc, WS_CLOSE_GOING_AWAY, reason);
362 +
363 + // Send close frame (never compressed)
364 + int result = websocket_protocol_send_frame(wsc, payload, payload_len, WS_OPCODE_CLOSE, false);
365 +
366 + return result;
367 +}
368 +
369 +// Send a ping frame
370 +int websocket_protocol_send_ping(WS_CLIENT *wsc, const char *data, size_t length) {
371 + if (!wsc)
372 + return -1;
373 +
374 + // Control frames max size is 125 bytes
375 + if (length > 125) {
376 + websocket_error(wsc, "Ping frame payload too large: %zu bytes (max: 125)",
377 + length);
378 + return -1;
379 + }
380 +
381 + // If no data provided, use empty ping
382 + if (!data || length == 0) {
383 + data = "";
384 + length = 0;
385 + }
386 +
387 + // Send ping frame (never compressed)
388 + return websocket_protocol_send_frame(wsc, data, length, WS_OPCODE_PING, false);
389 +}
390 +
391 +// Send a pong frame
392 +int websocket_protocol_send_pong(WS_CLIENT *wsc, const char *data, size_t length) {
393 + if (!wsc)
394 + return -1;
395 +
396 + // Control frames max size is 125 bytes
397 + if (length > 125) {
398 + websocket_error(wsc, "Pong frame payload too large: %zu bytes (max: 125)",
399 + length);
400 + return -1;
401 + }
402 +
403 + // If no data provided, use empty pong
404 + if (!data || length == 0) {
405 + data = "";
406 + length = 0;
407 + }
408 +
409 + // Send pong frame (never compressed)
410 + return websocket_protocol_send_frame(wsc, data, length, WS_OPCODE_PONG, false);
411 +}
src/web/websocket/websocket-thread.c new
+515
@@ -0,0 +1,515 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "daemon/daemon-service.h"
4 +#include "websocket-internal.h"
5 +
6 +static void websocket_thread_client_socket_error(WEBSOCKET_THREAD *wth, WS_CLIENT *wsc, const char *reason) {
7 + internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
8 +
9 + worker_is_busy(WORKERS_WEBSOCKET_SOCK_ERROR);
10 +
11 + websocket_debug(wsc, reason);
12 +
13 + // Send command to remove the client
14 + // Note: on_disconnect will be called in websocket_thread_remove_client
15 + websocket_thread_send_command(wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
16 +}
17 +
18 +// Add a client to a thread's poll
19 +static bool websocket_thread_add_client(WEBSOCKET_THREAD *wth, WS_CLIENT *wsc) {
20 + internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
21 +
22 + // Initialize compression with the parsed options
23 + websocket_compression_init(wsc);
24 + websocket_decompression_init(wsc);
25 +
26 + // Add client to the poll - use the socket fd directly
27 + bool added = nd_poll_add(wth->ndpl, wsc->sock.fd, ND_POLL_READ, wsc);
28 + if(!added) {
29 + websocket_error(wsc, "Failed to add client to poll");
30 + return false;
31 + }
32 +
33 + // Add client to the thread's client list
34 + DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(wth->clients, wsc, prev, next);
35 +
36 + return true;
37 +}
38 +
39 +static void websocket_thread_remove_client(WEBSOCKET_THREAD *wth, WS_CLIENT *wsc) {
40 + internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
41 +
42 + // Notify the protocol handler that the client is being disconnected
43 + if (wsc->on_disconnect) {
44 + websocket_debug(wsc, "Calling on_disconnect callback for protocol %s", WEBSOCKET_PROTOCOL_2str(wsc->protocol));
45 + wsc->on_disconnect(wsc);
46 + }
47 +
48 + // send a close frame (it won't do it if not allowed by the protocol)
49 + websocket_protocol_send_close(wsc, WS_CLOSE_NORMAL, "Connection closed by server");
50 +
51 + // If already in a closing state, just flush any pending data
52 + websocket_write_data(wsc);
53 +
54 + // Remove client from the poll - use socket fd directly
55 + bool removed = nd_poll_del(wth->ndpl, wsc->sock.fd);
56 + if(!removed) {
57 + websocket_debug(wsc, "Failed to remove client %zu from poll", wsc->id);
58 + }
59 +
60 + websocket_decompression_cleanup(wsc);
61 + websocket_compression_cleanup(wsc);
62 +
63 + // Remove client from the thread's client list
64 + DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(wth->clients, wsc, prev, next);
65 +
66 + // Lock the thread clients
67 + spinlock_lock(&wth->clients_spinlock);
68 +
69 + if(wth->clients_current > 0)
70 + wth->clients_current--;
71 +
72 + // Release the thread clients lock
73 + spinlock_unlock(&wth->clients_spinlock);
74 +
75 + websocket_debug(wsc, "Removed and resources freed", wth->id, wsc->id);
76 + websocket_client_free(wsc);
77 +}
78 +
79 +// Update a client's poll event flags
80 +bool websocket_thread_update_client_poll_flags(WS_CLIENT *wsc) {
81 + if(!wsc || !wsc->wth || wsc->sock.fd < 0)
82 + return false;
83 +
84 + internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
85 +
86 + nd_poll_event_t events = wsc->flush_and_remove_client ? 0 : ND_POLL_READ;
87 + if(cbuffer_next_unsafe(&wsc->out_buffer, NULL) > 0)
88 + events |= ND_POLL_WRITE;
89 +
90 + // Update poll events
91 + bool updated = nd_poll_upd(wsc->wth->ndpl, wsc->sock.fd, events);
92 + if(!updated)
93 + websocket_error(wsc, "Failed to update poll events for client");
94 +
95 + return updated;
96 +}
97 +
98 +struct pipe_header {
99 + uint8_t cmd;
100 + union {
101 + uint32_t id;
102 + uint32_t len;
103 + };
104 +};
105 +
106 +// Send command to a thread
107 +bool websocket_thread_send_command(WEBSOCKET_THREAD *wth, uint8_t cmd, uint32_t id) {
108 + if(!wth || wth->cmd.pipe[PIPE_WRITE] == -1) {
109 + netdata_log_error("WEBSOCKET[%zu]: Failed to send command - pipe is not initialized", wth->id);
110 + return false;
111 + }
112 +
113 + // Prepare command
114 + struct pipe_header header = {
115 + .cmd = cmd,
116 + .id = id,
117 + };
118 +
119 + // Lock command pipe for writing
120 + spinlock_lock(&wth->spinlock);
121 +
122 + // Write command header
123 + ssize_t bytes = write(wth->cmd.pipe[PIPE_WRITE], &header, sizeof(header));
124 + if(bytes != sizeof(header)) {
125 + netdata_log_error("WEBSOCKET[%zu]: Failed to write command header to pipe", wth->id);
126 + spinlock_unlock(&wth->spinlock);
127 + return false;
128 + }
129 +
130 + // Release command pipe
131 + spinlock_unlock(&wth->spinlock);
132 +
133 + return true;
134 +}
135 +
136 +bool websocket_thread_send_broadcast(WEBSOCKET_THREAD *wth, WEBSOCKET_OPCODE opcode, const char *message) {
137 + if(!wth || wth->cmd.pipe[PIPE_WRITE] == -1) {
138 + netdata_log_error("WEBSOCKET[%zu]: Failed to send command - pipe is not initialized", wth->id);
139 + return false;
140 + }
141 +
142 + uint32_t message_len = strlen(message);
143 +
144 + // Prepare command
145 + struct pipe_header header = {
146 + .cmd = WEBSOCKET_THREAD_CMD_BROADCAST,
147 + .len = sizeof(opcode) + message_len,
148 + };
149 +
150 + // Lock command pipe for writing
151 + spinlock_lock(&wth->spinlock);
152 +
153 + // Write command header
154 + ssize_t bytes = write(wth->cmd.pipe[PIPE_WRITE], &header, sizeof(header.cmd));
155 + if(bytes != sizeof(header.cmd)) {
156 + netdata_log_error("WEBSOCKET[%zu]: Failed to write command header to pipe", wth->id);
157 + spinlock_unlock(&wth->spinlock);
158 + return false;
159 + }
160 +
161 + // Write the opcode
162 + bytes = write(wth->cmd.pipe[PIPE_WRITE], &opcode, sizeof(opcode));
163 + if(bytes != sizeof(opcode)) {
164 + netdata_log_error("WEBSOCKET[%zu]: Failed to write broadcast opcode to pipe", wth->id);
165 + spinlock_unlock(&wth->spinlock);
166 + return false;
167 + }
168 +
169 + // Write the message
170 + bytes = write(wth->cmd.pipe[PIPE_WRITE], message, message_len);;
171 + if(bytes != message_len) {
172 + netdata_log_error("WEBSOCKET[%zu]: Failed to write broadcast message to pipe", wth->id);
173 + spinlock_unlock(&wth->spinlock);
174 + return false;
175 + }
176 +
177 + // Release command pipe
178 + spinlock_unlock(&wth->spinlock);
179 +
180 + return true;
181 +}
182 +
183 +static ssize_t read_pipe_block(int fd, void *buffer, size_t size) {
184 + char *buf = buffer;
185 + size_t total_read = 0;
186 +
187 + while (total_read < size) {
188 + ssize_t bytes = read(fd, buf + total_read, size - total_read);
189 +
190 + if (bytes < 0) {
191 + if (errno == EAGAIN || errno == EWOULDBLOCK) {
192 + // Non-blocking case, return what we've read so far
193 + return (ssize_t)total_read;
194 + }
195 +
196 + // Real error occurred
197 + return -1;
198 +
199 + }
200 + else if (bytes == 0)
201 + return (ssize_t)total_read;
202 +
203 + total_read += bytes;
204 + }
205 +
206 + return (ssize_t)total_read;
207 +}
208 +
209 +// Process a thread's command pipe
210 +static void websocket_thread_process_commands(WEBSOCKET_THREAD *wth) {
211 + internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
212 +
213 + struct pipe_header header;
214 +
215 + // Read all available commands
216 + for(;;) {
217 + // Read command header
218 +
219 + worker_is_busy(WORKERS_WEBSOCKET_CMD_READ);
220 +
221 + ssize_t bytes = read_pipe_block(wth->cmd.pipe[PIPE_READ], &header, sizeof(header));
222 + if(bytes <= 0) {
223 + if(bytes < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
224 + netdata_log_error("WEBSOCKET[%zu]: Failed to read command header from pipe", wth->id);
225 + }
226 + break;
227 + }
228 +
229 + if(bytes != sizeof(header)) {
230 + netdata_log_error("WEBSOCKET[%zu]: Read partial command header (%zd/%zu bytes)", wth->id, bytes, sizeof(header));
231 + break;
232 + }
233 +
234 + // Process command
235 + switch(header.cmd) {
236 + case WEBSOCKET_THREAD_CMD_EXIT:
237 + worker_is_busy(WORKERS_WEBSOCKET_CMD_EXIT);
238 + netdata_log_info("WEBSOCKET[%zu] received exit command", wth->id);
239 + return;
240 +
241 + case WEBSOCKET_THREAD_CMD_ADD_CLIENT: {
242 + worker_is_busy(WORKERS_WEBSOCKET_CMD_ADD);
243 + WS_CLIENT *wsc = websocket_client_find_by_id(header.id);
244 + if(!wsc) {
245 + netdata_log_error("WEBSOCKET[%zu]: Client %u not found for add command", wth->id, header.id);
246 + continue;
247 + }
248 + internal_fatal(wsc->wth != wth, "Client %u added to wrong thread", header.id);
249 + wsc->wth = wth;
250 + if (websocket_thread_add_client(wth, wsc)) {
251 + // Call the on_connect callback if provided to notify protocol handler of new client
252 + if (wsc->on_connect) {
253 + websocket_debug(wsc, "Calling on_connect callback for protocol %s", WEBSOCKET_PROTOCOL_2str(wsc->protocol));
254 + wsc->on_connect(wsc);
255 + }
256 + }
257 + break;
258 + }
259 +
260 + case WEBSOCKET_THREAD_CMD_REMOVE_CLIENT: {
261 + worker_is_busy(WORKERS_WEBSOCKET_CMD_DEL);
262 + WS_CLIENT *wsc = websocket_client_find_by_id(header.id);
263 + if(!wsc) {
264 + netdata_log_error("WEBSOCKET[%zu]: Client %u not found for remove command", wth->id, header.id);
265 + continue;
266 + }
267 +
268 + websocket_thread_remove_client(wth, wsc);
269 + break;
270 + }
271 +
272 + case WEBSOCKET_THREAD_CMD_BROADCAST: {
273 + worker_is_busy(WORKERS_WEBSOCKET_CMD_BROADCAST);
274 +
275 + WEBSOCKET_OPCODE opcode;
276 + bytes = read_pipe_block(wth->cmd.pipe[PIPE_READ], &opcode, sizeof(opcode));
277 + if(bytes != sizeof(opcode)) {
278 + netdata_log_error("WEBSOCKET[%zu]: Failed to read broadcast opcode from pipe", wth->id);
279 + continue;
280 + }
281 +
282 + uint32_t message_len = header.len - sizeof(opcode);
283 + char message[message_len + 1];
284 + bytes = read_pipe_block(wth->cmd.pipe[PIPE_READ], message, message_len);
285 + if(bytes != message_len) {
286 + netdata_log_error("WEBSOCKET[%zu]: Failed to read broadcast message from pipe", wth->id);
287 + continue;
288 + }
289 +
290 + // Ensure we have the complete message
291 + if(header.len != sizeof(WEBSOCKET_OPCODE) + message_len) {
292 + netdata_log_error("WEBSOCKET[%zu]: Broadcast command size mismatch", wth->id);
293 + continue;
294 + }
295 +
296 + // Send to all clients in this thread
297 + spinlock_lock(&wth->clients_spinlock);
298 +
299 + WS_CLIENT *wsc = wth->clients;
300 + while(wsc) {
301 + if(wsc->state == WS_STATE_OPEN) {
302 + websocket_send_message(wsc, message, message_len, opcode);
303 + }
304 + wsc = wsc->next;
305 + }
306 +
307 + spinlock_unlock(&wth->clients_spinlock);
308 + break;
309 + }
310 +
311 + default:
312 + worker_is_busy(WORKERS_WEBSOCKET_CMD_UNKNOWN);
313 + netdata_log_error("WEBSOCKET[%zu]: Unknown command %u", wth->id, header.cmd);
314 + break;
315 + }
316 + }
317 +}
318 +
319 +// Thread main function
320 +void *websocket_thread(void *ptr) {
321 + WEBSOCKET_THREAD *wth = (WEBSOCKET_THREAD *)ptr;
322 + wth->tid = gettid_uncached();
323 +
324 + worker_register("WEBSOCKET");
325 + worker_register_job_name(WORKERS_WEBSOCKET_POLL, "poll");
326 + worker_register_job_name(WORKERS_WEBSOCKET_CMD_READ, "cmd read");
327 + worker_register_job_name(WORKERS_WEBSOCKET_CMD_EXIT, "cmd exit");
328 + worker_register_job_name(WORKERS_WEBSOCKET_CMD_ADD, "cmd add");
329 + worker_register_job_name(WORKERS_WEBSOCKET_CMD_DEL, "cmd del");
330 + worker_register_job_name(WORKERS_WEBSOCKET_CMD_BROADCAST, "cmd bcast");
331 + worker_register_job_name(WORKERS_WEBSOCKET_CMD_UNKNOWN, "cmd unknown");
332 + worker_register_job_name(WORKERS_WEBSOCKET_SOCK_RECEIVE, "ws rcv");
333 + worker_register_job_name(WORKERS_WEBSOCKET_SOCK_SEND, "ws snd");
334 + worker_register_job_name(WORKERS_WEBSOCKET_SOCK_ERROR, "ws err");
335 + worker_register_job_name(WORKERS_WEBSOCKET_CLIENT_TIMEOUT, "client timeout");
336 + worker_register_job_name(WORKERS_WEBSOCKET_SEND_PING, "send ping");
337 + worker_register_job_name(WORKERS_WEBSOCKET_CLIENT_STUCK, "client stuck");
338 + worker_register_job_name(WORKERS_WEBSOCKET_INCOMPLETE_FRAME, "incomplete frame");
339 + worker_register_job_name(WORKERS_WEBSOCKET_COMPLETE_FRAME, "complete frame");
340 + worker_register_job_name(WORKERS_WEBSOCKET_MESSAGE, "message");
341 + worker_register_job_name(WORKERS_WEBSOCKET_MSG_PING, "rx ping");
342 + worker_register_job_name(WORKERS_WEBSOCKET_MSG_PONG, "rx pong");
343 + worker_register_job_name(WORKERS_WEBSOCKET_MSG_CLOSE, "rx close");
344 + worker_register_job_name(WORKERS_WEBSOCKET_MSG_INVALID, "rx invalid");
345 +
346 + time_t last_cleanup = now_monotonic_sec();
347 +
348 + // Main thread loop
349 + while(service_running(SERVICE_STREAMING) && !nd_thread_signaled_to_cancel()) {
350 +
351 + worker_is_idle();
352 +
353 + // Poll for events
354 + nd_poll_result_t ev;
355 + int rc = nd_poll_wait(wth->ndpl, 100, &ev); // 100ms timeout
356 +
357 + worker_is_busy(WORKERS_WEBSOCKET_POLL);
358 +
359 + if(rc < 0) {
360 + if(errno == EAGAIN || errno == EINTR)
361 + continue;
362 +
363 + netdata_log_error("WEBSOCKET[%zu]: Poll error: %s", wth->id, strerror(errno));
364 + break;
365 + }
366 +
367 + // Process poll events
368 + if(rc > 0) {
369 + // Handle command pipe
370 + if(ev.data == &wth->cmd) {
371 + if(ev.events & ND_POLL_READ) {
372 + // Read and process commands
373 + websocket_thread_process_commands(wth);
374 + }
375 + continue;
376 + }
377 +
378 + // Handle client events
379 + WS_CLIENT *wsc = (WS_CLIENT *)ev.data;
380 + if(!wsc) {
381 + netdata_log_error("WEBSOCKET[%zu]: Poll event with NULL client data", wth->id);
382 + continue;
383 + }
384 +
385 + // Check for errors
386 + if(ev.events & ND_POLL_HUP) {
387 + websocket_thread_client_socket_error(wth, wsc, "Client hangup");
388 + continue;
389 + }
390 + if(ev.events & ND_POLL_ERROR) {
391 + websocket_thread_client_socket_error(wth, wsc, "Socket error");
392 + continue;
393 + }
394 +
395 + // Process read events
396 + if(ev.events & ND_POLL_READ) {
397 + if(websocket_receive_data(wsc) < 0) {
398 + websocket_thread_client_socket_error(wth, wsc, "Failed to receive data");
399 + continue;
400 + }
401 + }
402 +
403 + // Process write events
404 + if(ev.events & ND_POLL_WRITE) {
405 + if(websocket_write_data(wsc) < 0) {
406 + websocket_thread_client_socket_error(wth, wsc, "Failed to send data");
407 + continue;
408 + }
409 +
410 + // Check if this client is waiting to be closed after flushing outgoing data
411 + if(wsc->flush_and_remove_client && cbuffer_used_size_unsafe(&wsc->out_buffer) == 0) {
412 + // All data flushed - remove client
413 + websocket_thread_remove_client(wth, wsc);
414 + }
415 + }
416 + }
417 +
418 + worker_is_idle();
419 +
420 + // Periodic cleanup and health checks (every 30 seconds)
421 + time_t now = now_monotonic_sec();
422 + if(now - last_cleanup > 30) {
423 + // Iterate through all clients in this thread
424 + spinlock_lock(&wth->clients_spinlock);
425 +
426 + WS_CLIENT *wsc = wth->clients;
427 + while(wsc) {
428 + WS_CLIENT *next = wsc->next; // Save next in case we remove this client
429 +
430 + if(wsc->state == WS_STATE_OPEN) {
431 + // Check if client is idle (no activity for over 120 seconds)
432 + if(now - wsc->last_activity_t > 120) {
433 + // Client is idle - send a ping to check if it's still alive
434 + worker_is_busy(WORKERS_WEBSOCKET_SEND_PING);
435 + websocket_protocol_send_ping(wsc, NULL, 0);
436 +
437 + // If no activity for over 300 seconds (5 minutes), consider it dead
438 + if(now - wsc->last_activity_t > 300) {
439 + worker_is_busy(WORKERS_WEBSOCKET_CLIENT_TIMEOUT);
440 + websocket_error(wsc, "Client timed out (no activity for over 5 minutes)");
441 + websocket_protocol_exception(wsc, WS_CLOSE_GOING_AWAY, "Timeout - no activity");
442 + }
443 + }
444 + // For normal clients, send periodic pings (every 60 seconds)
445 + else if(now - wsc->last_activity_t > 60) {
446 + worker_is_busy(WORKERS_WEBSOCKET_SEND_PING);
447 + websocket_protocol_send_ping(wsc, NULL, 0);
448 + }
449 + }
450 + else if(wsc->state == WS_STATE_CLOSING_SERVER || wsc->state == WS_STATE_CLOSING_CLIENT) {
451 + // If a client is in any CLOSING state for more than 5 seconds, force close it
452 + if(now - wsc->last_activity_t > 5) {
453 + worker_is_busy(WORKERS_WEBSOCKET_CLIENT_STUCK);
454 + websocket_error(wsc, "Forcing close (stuck in %s state)",
455 + wsc->state == WS_STATE_CLOSING_SERVER ? "CLOSING_SERVER" : "CLOSING_CLIENT");
456 + websocket_thread_send_command(wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
457 + }
458 + }
459 +
460 + wsc = next;
461 + }
462 +
463 + spinlock_unlock(&wth->clients_spinlock);
464 +
465 + last_cleanup = now;
466 + }
467 + }
468 +
469 + netdata_log_info("WEBSOCKET[%zu] exiting", wth->id);
470 +
471 + // Clean up any remaining clients
472 + spinlock_lock(&wth->clients_spinlock);
473 +
474 + // Close all clients in this thread
475 + WS_CLIENT *wsc = wth->clients;
476 + while(wsc) {
477 + WS_CLIENT *next = wsc->next;
478 +
479 + websocket_protocol_send_close(wsc, WS_CLOSE_GOING_AWAY, "Server shutting down");
480 + websocket_write_data(wsc);
481 + websocket_thread_remove_client(wth, wsc);
482 +
483 + wsc = next;
484 + }
485 +
486 + // Reset thread's client list
487 + wth->clients = NULL;
488 + wth->clients_current = 0;
489 +
490 + spinlock_unlock(&wth->clients_spinlock);
491 +
492 + // Cleanup poll resources
493 + if(wth->ndpl) {
494 + nd_poll_destroy(wth->ndpl);
495 + wth->ndpl = NULL;
496 + }
497 +
498 + // Cleanup command pipe
499 + if(wth->cmd.pipe[PIPE_READ] != -1) {
500 + close(wth->cmd.pipe[PIPE_READ]);
501 + wth->cmd.pipe[PIPE_READ] = -1;
502 + }
503 +
504 + if(wth->cmd.pipe[PIPE_WRITE] != -1) {
505 + close(wth->cmd.pipe[PIPE_WRITE]);
506 + wth->cmd.pipe[PIPE_WRITE] = -1;
507 + }
508 +
509 + // Mark thread as not running
510 + spinlock_lock(&wth->spinlock);
511 + wth->running = false;
512 + spinlock_unlock(&wth->spinlock);
513 +
514 + return NULL;
515 +}
src/web/websocket/websocket-thread.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEBSOCKET_THREAD_H
4 +#define NETDATA_WEBSOCKET_THREAD_H
5 +
6 +// This file is kept for backward compatibility
7 +// All contents have been moved to websocket-internal.h
8 +
9 +#include "websocket-internal.h"
10 +
11 +#endif // NETDATA_WEBSOCKET_THREAD_H
\ No newline at end of file
src/web/websocket/websocket-utils.c new
+105
@@ -0,0 +1,105 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-internal.h"
4 +
5 +// Debug log function with client, message, and frame IDs
6 +void websocket_debug(WS_CLIENT *wsc __maybe_unused, const char *format __maybe_unused, ...) {
7 +#ifdef NETDATA_INTERNAL_CHECKS
8 + if (!wsc || !format)
9 + return;
10 +
11 + char formatted_message[1024];
12 + va_list args;
13 + va_start(args, format);
14 + vsnprintf(formatted_message, sizeof(formatted_message), format, args);
15 + va_end(args);
16 +
17 + // Format the debug message with client, message, and frame IDs
18 + netdata_log_debug(D_WEBSOCKET, "WEBSOCKET: C=%u M=%zu F=%zu %s",
19 + wsc->id, wsc->message_id, wsc->frame_id, formatted_message);
20 +#endif /* NETDATA_INTERNAL_CHECKS */
21 +}
22 +
23 +// Info log function with client, message, and frame IDs
24 +void websocket_info(WS_CLIENT *wsc, const char *format, ...) {
25 + if (!wsc || !format)
26 + return;
27 +
28 + char formatted_message[1024];
29 + va_list args;
30 + va_start(args, format);
31 + vsnprintf(formatted_message, sizeof(formatted_message), format, args);
32 + va_end(args);
33 +
34 + // Format the info message with client, message, and frame IDs
35 + netdata_log_info("WEBSOCKET: C=%u M=%zu F=%zu %s",
36 + wsc->id, wsc->message_id, wsc->frame_id, formatted_message);
37 +}
38 +
39 +// Error log function with client, message, and frame IDs
40 +void websocket_error(WS_CLIENT *wsc, const char *format, ...) {
41 + if (!wsc || !format)
42 + return;
43 +
44 + char formatted_message[1024];
45 + va_list args;
46 + va_start(args, format);
47 + vsnprintf(formatted_message, sizeof(formatted_message), format, args);
48 + va_end(args);
49 +
50 + // Format the error message with client, message, and frame IDs
51 + netdata_log_error("WEBSOCKET: C=%u M=%zu F=%zu %s",
52 + wsc->id, wsc->message_id, wsc->frame_id, formatted_message);
53 +}
54 +
55 +// Debug function that logs a message and dumps payload data for debugging
56 +void websocket_dump_debug(WS_CLIENT *wsc __maybe_unused, const char *payload __maybe_unused,
57 + size_t payload_length __maybe_unused, const char *format __maybe_unused, ...) {
58 +#ifdef NETDATA_INTERNAL_CHECKS
59 + if (!wsc || !format)
60 + return;
61 +
62 + // Format the primary message
63 + char formatted_message[1024];
64 + va_list args;
65 + va_start(args, format);
66 + vsnprintf(formatted_message, sizeof(formatted_message), format, args);
67 + va_end(args);
68 +
69 + // Handle empty payloads explicitly (log message but no hex dump)
70 + if (payload_length == 0) {
71 + netdata_log_debug(D_WEBSOCKET, "WEBSOCKET: C=%u M=%zu F=%zu %s (EMPTY PAYLOAD - 0 bytes)",
72 + wsc->id, wsc->message_id, wsc->frame_id, formatted_message);
73 + return;
74 + }
75 +
76 + // If payload is provided and not empty, create and log a hex dump
77 + if (payload && payload_length > 0) {
78 + size_t bytes_to_dump = (payload_length < 32) ? payload_length : 32;
79 +
80 + char hex_dump[bytes_to_dump * 2 + 1];
81 + char ascii_dump[bytes_to_dump + 1];
82 +
83 + // Payload check is redundant as we already have it in the outer if
84 +
85 + // Create the hex dump
86 + size_t i = 0;
87 + for (i = 0; i < bytes_to_dump; i++) {
88 + unsigned char c = (unsigned char)payload[i];
89 + sprintf(hex_dump + i * 2, "%02x", c);
90 + ascii_dump[i] = (isprint(c) ? c : '.');
91 + }
92 +
93 + hex_dump[i * 2] = '\0';
94 + ascii_dump[i] = '\0';
95 +
96 + // Log the hex dump
97 + netdata_log_debug(D_WEBSOCKET, "WEBSOCKET: C=%u M=%zu F=%zu %s DUMP %zu/%zu: HEX:[%s]%s, ASCII:[%s]%s",
98 + wsc->id, wsc->message_id, wsc->frame_id,
99 + formatted_message,
100 + bytes_to_dump, payload_length,
101 + hex_dump, payload_length > bytes_to_dump ? "..." : "",
102 + ascii_dump, payload_length > bytes_to_dump ? "..." : "");
103 + }
104 +#endif /* NETDATA_INTERNAL_CHECKS */
105 +}
src/web/websocket/websocket.c new
+264
@@ -0,0 +1,264 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "websocket-internal.h"
4 +#include "websocket-echo.h"
5 +#include "websocket-jsonrpc.h"
6 +#include "../mcp/adapters/mcp-websocket.h"
7 +
8 +ENUM_STR_MAP_DEFINE(WEBSOCKET_PROTOCOL) = {
9 + { .id = WS_PROTOCOL_JSONRPC, .name = "jsonrpc" },
10 + { .id = WS_PROTOCOL_ECHO, .name = "echo" },
11 + { .id = WS_PROTOCOL_MCP, .name = "mcp" },
12 + { .id = WS_PROTOCOL_UNKNOWN, .name = "unknown" },
13 +
14 + // terminator
15 + { .name = NULL, .id = 0 }
16 +};
17 +ENUM_STR_DEFINE_FUNCTIONS(WEBSOCKET_PROTOCOL, WS_PROTOCOL_UNKNOWN, "unknown");
18 +
19 +ENUM_STR_MAP_DEFINE(WEBSOCKET_STATE) = {
20 + { .id = WS_STATE_HANDSHAKE, .name = "handshake" },
21 + { .id = WS_STATE_OPEN, .name = "open" },
22 + { .id = WS_STATE_CLOSING_SERVER, .name = "closing_server" },
23 + { .id = WS_STATE_CLOSING_CLIENT, .name = "closing_client" },
24 + { .id = WS_STATE_CLOSED, .name = "closed" },
25 +
26 + // terminator
27 + { .name = NULL, .id = 0 }
28 +};
29 +ENUM_STR_DEFINE_FUNCTIONS(WEBSOCKET_STATE, WS_STATE_CLOSED, "closed");
30 +
31 +ENUM_STR_MAP_DEFINE(WEBSOCKET_OPCODE) = {
32 + { .id = WS_OPCODE_CONTINUATION, .name = "continuation" },
33 + { .id = WS_OPCODE_TEXT, .name = "text" },
34 + { .id = WS_OPCODE_BINARY, .name = "binary" },
35 + { .id = WS_OPCODE_CLOSE, .name = "close" },
36 + { .id = WS_OPCODE_PING, .name = "ping" },
37 + { .id = WS_OPCODE_PONG, .name = "pong" },
38 +
39 + // terminator
40 + { .name = NULL, .id = 0 }
41 +};
42 +ENUM_STR_DEFINE_FUNCTIONS(WEBSOCKET_OPCODE, WS_OPCODE_TEXT, "text");
43 +
44 +ENUM_STR_MAP_DEFINE(WEBSOCKET_CLOSE_CODE) = {
45 + // Standard WebSocket close codes
46 + { .id = WS_CLOSE_NORMAL, .name = "normal" },
47 + { .id = WS_CLOSE_GOING_AWAY, .name = "going_away" },
48 + { .id = WS_CLOSE_PROTOCOL_ERROR, .name = "protocol_error" },
49 + { .id = WS_CLOSE_UNSUPPORTED_DATA, .name = "unsupported_data" },
50 + { .id = WS_CLOSE_RESERVED, .name = "reserved" },
51 + { .id = WS_CLOSE_NO_STATUS, .name = "no_status" },
52 + { .id = WS_CLOSE_ABNORMAL, .name = "abnormal" },
53 + { .id = WS_CLOSE_INVALID_PAYLOAD, .name = "invalid_payload" },
54 + { .id = WS_CLOSE_POLICY_VIOLATION, .name = "policy_violation" },
55 + { .id = WS_CLOSE_MESSAGE_TOO_BIG, .name = "message_too_big" },
56 + { .id = WS_CLOSE_EXTENSION_MISSING, .name = "extension_missing" },
57 + { .id = WS_CLOSE_INTERNAL_ERROR, .name = "internal_error" },
58 + { .id = WS_CLOSE_TLS_HANDSHAKE, .name = "tls_handshake_error" },
59 +
60 + // Netdata-specific close codes
61 + { .id = WS_CLOSE_NETDATA_TIMEOUT, .name = "timeout" },
62 + { .id = WS_CLOSE_NETDATA_SHUTDOWN, .name = "shutdown" },
63 + { .id = WS_CLOSE_NETDATA_REJECTED, .name = "rejected" },
64 + { .id = WS_CLOSE_NETDATA_RATE_LIMIT,.name = "rate_limit" },
65 +
66 + // terminator
67 + { .name = NULL, .id = 0 }
68 +};
69 +ENUM_STR_DEFINE_FUNCTIONS(WEBSOCKET_CLOSE_CODE, WS_CLOSE_NORMAL, "normal");
70 +
71 +// Private structure for WebSocket server state
72 +struct websocket_server {
73 + WS_CLIENTS_JudyLSet clients; // JudyL array of WebSocket clients
74 + size_t client_id_counter; // Counter for generating unique client IDs
75 + size_t active_clients; // Number of active clients
76 + SPINLOCK spinlock; // Spinlock to protect the registry
77 +};
78 +
79 +// The global (but private) instance of the WebSocket server state
80 +static struct websocket_server ws_server = (struct websocket_server){
81 + .clients = { 0 },
82 + .client_id_counter = 0,
83 + .active_clients = 0,
84 + .spinlock = SPINLOCK_INITIALIZER
85 +};
86 +
87 +// Initialize WebSocket subsystem
88 +void websocket_initialize(void) {
89 + debug_flags |= D_WEBSOCKET;
90 +
91 + // Initialize thread system
92 + websocket_threads_init();
93 +
94 + // Initialize protocol handlers
95 + websocket_jsonrpc_initialize();
96 + websocket_echo_initialize();
97 + mcp_websocket_adapter_initialize();
98 +
99 + netdata_log_info("WebSocket server subsystem initialized");
100 +}
101 +
102 +// Create a new WebSocket client with a unique ID
103 +NEVERNULL
104 +WS_CLIENT *websocket_client_create(void) {
105 + WS_CLIENT *wsc = callocz(1, sizeof(WS_CLIENT));
106 +
107 + spinlock_lock(&ws_server.spinlock);
108 + wsc->id = ++ws_server.client_id_counter; // Generate unique ID
109 + spinlock_unlock(&ws_server.spinlock);
110 +
111 + wsc->connected_t = now_realtime_sec();
112 + wsc->last_activity_t = wsc->connected_t;
113 +
114 + // initialize callbacks to NULL
115 + wsc->on_connect = NULL;
116 + wsc->on_message = NULL;
117 + wsc->on_close = NULL;
118 + wsc->on_disconnect = NULL;
119 +
120 + // initialize the ND_SOCK with the web server's SSL context
121 + nd_sock_init(&wsc->sock, netdata_ssl_web_server_ctx, false);
122 +
123 + // Initialize circular buffers for I/O with WebSocket-specific sizes and max limits
124 + cbuffer_init(&wsc->in_buffer, WEBSOCKET_IN_BUFFER_INITIAL_SIZE, WEBSOCKET_IN_BUFFER_MAX_SIZE, NULL);
125 + cbuffer_init(&wsc->out_buffer, WEBSOCKET_OUT_BUFFER_INITIAL_SIZE, WEBSOCKET_OUT_BUFFER_MAX_SIZE, NULL);
126 +
127 + // Initialize pre-allocated message buffer
128 + wsb_init(&wsc->payload, WEBSOCKET_PAYLOAD_INITIAL_SIZE);
129 +
130 + // Initialize uncompressed buffer with a larger size since decompressed data can expand
131 + // For compressed content, the expanded data can be much larger than the input
132 + wsb_init(&wsc->u_payload, WEBSOCKET_UNPACKED_INITIAL_SIZE);
133 +
134 + // Set the initial message state
135 + wsc->opcode = WS_OPCODE_TEXT; // Default opcode
136 + wsc->is_compressed = false;
137 + wsc->message_complete = true; // Not in a fragmented sequence initially
138 + wsc->frame_id = 0;
139 + wsc->message_id = 0;
140 + wsc->compression = WEBSOCKET_COMPRESSION_DEFAULTS;
141 +
142 + return wsc;
143 +}
144 +
145 +// Free a WebSocket client
146 +void websocket_client_free(WS_CLIENT *wsc) {
147 + if (!wsc)
148 + return;
149 +
150 + // First unregister from the client registry
151 + websocket_client_unregister(wsc);
152 +
153 + // We MUST make sure the socket is not in the poll before closing it
154 + // otherwise kernel structures may be corrupted due to socket reuse
155 + if(wsc->wth && wsc->wth->ndpl && wsc->sock.fd >= 0)
156 + nd_poll_del(wsc->wth->ndpl, wsc->sock.fd);
157 +
158 + // Close socket using ND_SOCK abstraction
159 + nd_sock_close(&wsc->sock);
160 +
161 + // Free circular buffers
162 + cbuffer_cleanup(&wsc->in_buffer);
163 + cbuffer_cleanup(&wsc->out_buffer);
164 +
165 + // Cleanup pre-allocated message and uncompressed buffers
166 + wsb_cleanup(&wsc->payload);
167 + wsb_cleanup(&wsc->u_payload);
168 +
169 + // Clean up compression resources if needed
170 + websocket_compression_cleanup(wsc);
171 +
172 + freez(wsc);
173 +}
174 +
175 +// Register a WebSocket client in the registry
176 +bool websocket_client_register(WS_CLIENT *wsc) {
177 + if (!wsc || wsc->id == 0)
178 + return false;
179 +
180 + spinlock_lock(&ws_server.spinlock);
181 +
182 + int added = WS_CLIENTS_SET(&ws_server.clients, wsc->id, wsc);
183 + if (!added) {
184 + ws_server.active_clients++;
185 + websocket_debug(wsc, "WebSocket client registered, total clients: %u", ws_server.active_clients);
186 + }
187 +
188 + spinlock_unlock(&ws_server.spinlock);
189 +
190 + return added;
191 +}
192 +
193 +// Unregister a WebSocket client from the registry
194 +void websocket_client_unregister(WS_CLIENT *wsc) {
195 + if (!wsc || wsc->id == 0)
196 + return;
197 +
198 + spinlock_lock(&ws_server.spinlock);
199 +
200 + WS_CLIENT *existing = WS_CLIENTS_GET(&ws_server.clients, wsc->id);
201 + if (existing && existing == wsc) {
202 + WS_CLIENTS_DEL(&ws_server.clients, wsc->id);
203 + if (ws_server.active_clients > 0)
204 + ws_server.active_clients--;
205 +
206 + websocket_debug(wsc,"WebSocket client unregistered, total clients: %zu", ws_server.active_clients);
207 + }
208 +
209 + spinlock_unlock(&ws_server.spinlock);
210 +}
211 +
212 +// Find a WebSocket client by ID
213 +ALWAYS_INLINE
214 +WS_CLIENT *websocket_client_find_by_id(size_t id) {
215 + if (id == 0)
216 + return NULL;
217 +
218 + WS_CLIENT *wsc = NULL;
219 +
220 + spinlock_lock(&ws_server.spinlock);
221 + wsc = WS_CLIENTS_GET(&ws_server.clients, id);
222 + spinlock_unlock(&ws_server.spinlock);
223 +
224 + return wsc;
225 +}
226 +
227 +// Broadcast a message to all connected WebSocket clients
228 +int websocket_broadcast_message(const char *message, WEBSOCKET_OPCODE opcode) {
229 + if (!message || (opcode != WS_OPCODE_TEXT && opcode != WS_OPCODE_BINARY))
230 + return -1;
231 +
232 + int success_count = 0;
233 +
234 + // Send broadcast command to all active threads
235 + for(size_t i = 0; i < WEBSOCKET_MAX_THREADS; i++) {
236 + if(websocket_threads[i].thread && websocket_threads[i].running) {
237 + if(websocket_thread_send_broadcast(&websocket_threads[i], opcode, message)) {
238 + success_count++;
239 + }
240 + }
241 + }
242 +
243 + return success_count;
244 +}
245 +
246 +// Send a WebSocket message to the client
247 +int websocket_send_message(WS_CLIENT *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode) {
248 + if (!wsc || !message || wsc->state != WS_STATE_OPEN)
249 + return -1;
250 +
251 + // Use the appropriate protocol function based on opcode
252 + if (opcode == WS_OPCODE_TEXT) {
253 + return websocket_protocol_send_text(wsc, message);
254 + } else if (opcode == WS_OPCODE_BINARY) {
255 + return websocket_protocol_send_binary(wsc, message, length);
256 + } else {
257 + // For other opcodes, use the generic frame sender
258 + bool use_compression = wsc->compression.enabled &&
259 + !websocket_frame_is_control_opcode(opcode) &&
260 + length >= WS_COMPRESS_MIN_SIZE;
261 +
262 + return websocket_protocol_send_frame(wsc, message, length, opcode, use_compression);
263 + }
264 +}
src/web/websocket/websocket.h new
+106
@@ -0,0 +1,106 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_WEB_SERVER_WEBSOCKET_H
4 +#define NETDATA_WEB_SERVER_WEBSOCKET_H 1
5 +
6 +#include "libnetdata/libnetdata.h"
7 +
8 +// WebSocket subprotocols supported by Netdata
9 +typedef enum __attribute__((packed)) {
10 + WS_PROTOCOL_DEFAULT = 0, // the protocol is selected from the url
11 + WS_PROTOCOL_UNKNOWN, // Unknown or unsupported protocol
12 + WS_PROTOCOL_JSONRPC, // JSON-RPC protocol
13 + WS_PROTOCOL_ECHO, // Echo protocol
14 + WS_PROTOCOL_MCP, // Model Context Protocol
15 +} WEBSOCKET_PROTOCOL;
16 +ENUM_STR_DEFINE_FUNCTIONS_EXTERN(WEBSOCKET_PROTOCOL);
17 +
18 +// WebSocket extensions supported by Netdata
19 +typedef enum __attribute__((packed)) {
20 + // RFC 7692
21 + WS_EXTENSION_NONE = 0, // No extensions
22 + WS_EXTENSION_PERMESSAGE_DEFLATE = (1 << 0), // permessage-deflate
23 + WS_EXTENSION_CLIENT_NO_CONTEXT_TAKEOVER = (1 << 1), // client_no_context_takeover
24 + WS_EXTENSION_SERVER_NO_CONTEXT_TAKEOVER = (1 << 2), // server_no_context_takeover
25 + WS_EXTENSION_SERVER_MAX_WINDOW_BITS = (1 << 3), // server_max_window_bits
26 + WS_EXTENSION_CLIENT_MAX_WINDOW_BITS = (1 << 4) // client_max_window_bits
27 +} WEBSOCKET_EXTENSION;
28 +
29 +// Forward declarations
30 +struct web_client;
31 +struct websocket_server_client;
32 +
33 +// WebSocket connection state
34 +typedef enum __attribute__((packed)) {
35 + WS_STATE_HANDSHAKE = 0, // Initial handshake in progress
36 + WS_STATE_OPEN = 1, // Connection established
37 + WS_STATE_CLOSING_SERVER = 2, // Server initiated closing handshake
38 + WS_STATE_CLOSING_CLIENT = 3, // Client initiated closing handshake
39 + WS_STATE_CLOSED = 4 // Connection closed
40 +} WEBSOCKET_STATE;
41 +ENUM_STR_DEFINE_FUNCTIONS_EXTERN(WEBSOCKET_STATE);
42 +
43 +// WebSocket message types (opcodes) as per RFC 6455
44 +typedef enum __attribute__((packed)) {
45 + WS_OPCODE_CONTINUATION = 0x0,
46 + WS_OPCODE_TEXT = 0x1,
47 + WS_OPCODE_BINARY = 0x2,
48 + WS_OPCODE_CLOSE = 0x8,
49 + WS_OPCODE_PING = 0x9,
50 + WS_OPCODE_PONG = 0xA
51 +} WEBSOCKET_OPCODE;
52 +ENUM_STR_DEFINE_FUNCTIONS_EXTERN(WEBSOCKET_OPCODE);
53 +
54 +// WebSocket close codes as per RFC 6455
55 +typedef enum __attribute__((packed)) {
56 + // Standard WebSocket close codes
57 + WS_CLOSE_NORMAL = 1000, // Normal closure, meaning the purpose for which the connection was established has been fulfilled
58 + WS_CLOSE_GOING_AWAY = 1001, // Server/client going away (such as server shutdown or browser navigating away)
59 + WS_CLOSE_PROTOCOL_ERROR = 1002, // Protocol error
60 + WS_CLOSE_UNSUPPORTED_DATA = 1003, // Client received data it couldn't accept (e.g., server sent binary data when client only supports text)
61 + WS_CLOSE_RESERVED = 1004, // Reserved. Specific meaning might be defined in the future.
62 + WS_CLOSE_NO_STATUS = 1005, // No status code was provided even though one was expected
63 + WS_CLOSE_ABNORMAL = 1006, // Connection closed abnormally (no close frame received)
64 + WS_CLOSE_INVALID_PAYLOAD = 1007, // Frame payload data is invalid (e.g., non-UTF-8 data in a text frame)
65 + WS_CLOSE_POLICY_VIOLATION = 1008, // Generic message received that violates policy
66 + WS_CLOSE_MESSAGE_TOO_BIG = 1009, // Message too big to process
67 + WS_CLOSE_EXTENSION_MISSING = 1010, // Client expected the server to negotiate one or more extensions, but server didn't
68 + WS_CLOSE_INTERNAL_ERROR = 1011, // Server encountered an unexpected condition preventing it from fulfilling the request
69 + WS_CLOSE_TLS_HANDSHAKE = 1015, // Transport Layer Security (TLS) handshake failure
70 +
71 + // Netdata-specific close codes (4000-4999 range is available for private use)
72 + WS_CLOSE_NETDATA_TIMEOUT = 4000, // Client timed out due to inactivity
73 + WS_CLOSE_NETDATA_SHUTDOWN = 4001, // Server is shutting down
74 + WS_CLOSE_NETDATA_REJECTED = 4002, // Connection rejected by server
75 + WS_CLOSE_NETDATA_RATE_LIMIT= 4003 // Client exceeded rate limit
76 +} WEBSOCKET_CLOSE_CODE;
77 +ENUM_STR_DEFINE_FUNCTIONS_EXTERN(WEBSOCKET_CLOSE_CODE);
78 +
79 +/**
80 + * WebSocket Protocol Handler Callbacks
81 + *
82 + * These callbacks are invoked when specific events occur during the WebSocket lifecycle:
83 + *
84 + * - on_connect: Called when a client successfully connects and is ready to exchange messages.
85 + * This happens after the WebSocket handshake is complete and the client is added to a thread.
86 + * Use this callback to welcome the client or initialize any protocol-specific state.
87 + *
88 + * - on_message: Called when a complete message is received from the client.
89 + * This is where the protocol processes incoming messages from clients.
90 + *
91 + * - on_close: Called BEFORE sending a close frame to the client.
92 + * This gives the protocol a chance to inject a final message before the connection closes.
93 + *
94 + * - on_disconnect: Called when a client is about to be disconnected.
95 + * Use this callback to clean up any protocol-specific state for the client.
96 + */
97 +
98 +// Public WebSocket API functions
99 +
100 +// WebSocket detection and handshake
101 +short int websocket_handle_handshake(struct web_client *w);
102 +
103 +// Initialize the WebSocket subsystem
104 +void websocket_initialize(void);
105 +
106 +#endif // NETDATA_WEB_SERVER_WEBSOCKET_H