master
c 951 lines 29.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "aclk_util.h"
4
5 #include "aclk_proxy.h"
6
7 #include "database/rrd.h"
8
9 usec_t aclk_session_newarch = 0;
10
11 aclk_env_t *aclk_env = NULL;
12
13 void aclk_sensitive_memzero(void *ptr, size_t len)
14 {
15 if (!ptr || len == 0)
16 return;
17
18 volatile unsigned char *p = (volatile unsigned char *)ptr;
19 while (len--)
20 *p++ = 0;
21 }
22
23 void aclk_sensitive_free(char **ptr)
24 {
25 if (!ptr || !*ptr)
26 return;
27
28 size_t len = strlen(*ptr);
29 aclk_sensitive_memzero(*ptr, len);
30 freez(*ptr);
31 *ptr = NULL;
32 }
33
34 aclk_encoding_type_t aclk_encoding_type_t_from_str(const char *str) {
35 if (!strcmp(str, "json")) {
36 return ACLK_ENC_JSON;
37 }
38 if (!strcmp(str, "proto")) {
39 return ACLK_ENC_PROTO;
40 }
41 return ACLK_ENC_UNKNOWN;
42 }
43
44 aclk_transport_type_t aclk_transport_type_t_from_str(const char *str) {
45 if (!strcmp(str, "MQTTv3")) {
46 return ACLK_TRP_MQTT_3_1_1;
47 }
48 if (!strcmp(str, "MQTTv5")) {
49 return ACLK_TRP_MQTT_5;
50 }
51 return ACLK_TRP_UNKNOWN;
52 }
53
54 void aclk_transport_desc_t_destroy(aclk_transport_desc_t *trp_desc) {
55 freez(trp_desc->endpoint);
56 }
57
58 void aclk_env_t_destroy(aclk_env_t *env) {
59 freez(env->auth_endpoint);
60 if (env->transports) {
61 for (size_t i = 0; i < env->transport_count; i++) {
62 if(env->transports[i]) {
63 aclk_transport_desc_t_destroy(env->transports[i]);
64 freez(env->transports[i]);
65 env->transports[i] = NULL;
66 }
67 }
68 freez(env->transports);
69 }
70 if (env->capabilities) {
71 for (size_t i = 0; i < env->capability_count; i++)
72 freez(env->capabilities[i]);
73 freez(env->capabilities);
74 }
75 }
76
77 int aclk_env_has_capa(const char *capa)
78 {
79 for (int i = 0; i < (int) aclk_env->capability_count; i++) {
80 if (!strcasecmp(capa, aclk_env->capabilities[i]))
81 return 1;
82 }
83 return 0;
84 }
85
86 #ifdef ACLK_LOG_CONVERSATION_DIR
87 volatile int aclk_conversation_log_counter = 0;
88 #endif
89
90 #define ACLK_TOPIC_PREFIX "/agent/"
91
92 struct aclk_topic {
93 enum aclk_topics topic_id;
94 // as received from cloud - we keep this for
95 // eventual topic list update when claim_id changes
96 char *topic_recvd;
97 // constructed topic
98 char *topic;
99 };
100
101 // This helps to cache finalized topics (assembled with claim_id)
102 // to not have to alloc or create buffer and construct topic every
103 // time message is sent as in old ACLK
104 static struct aclk_topic **aclk_topic_cache = NULL;
105 static size_t aclk_topic_cache_items = 0;
106
107 void free_topic_cache(void)
108 {
109 if (aclk_topic_cache) {
110 for (size_t i = 0; i < aclk_topic_cache_items; i++) {
111 freez(aclk_topic_cache[i]->topic);
112 freez(aclk_topic_cache[i]->topic_recvd);
113 freez(aclk_topic_cache[i]);
114 }
115 freez(aclk_topic_cache);
116 aclk_topic_cache = NULL;
117 aclk_topic_cache_items = 0;
118 }
119 }
120
121 #define JSON_TOPIC_KEY_TOPIC "topic"
122 #define JSON_TOPIC_KEY_NAME "name"
123
124 struct topic_name {
125 enum aclk_topics id;
126 // cloud name - how is it called
127 // in answer to /password endpoint
128 const char *name;
129 } topic_names[] = {
130 { .id = ACLK_TOPICID_CHART, .name = "chart" },
131 { .id = ACLK_TOPICID_ALARMS, .name = "alarms" },
132 { .id = ACLK_TOPICID_METADATA, .name = "meta" },
133 { .id = ACLK_TOPICID_COMMAND, .name = "inbox-cmd" },
134 { .id = ACLK_TOPICID_AGENT_CONN, .name = "agent-connection" },
135 { .id = ACLK_TOPICID_CMD_NG_V1, .name = "inbox-cmd-v1" },
136 { .id = ACLK_TOPICID_CREATE_NODE, .name = "create-node-instance" },
137 { .id = ACLK_TOPICID_NODE_CONN, .name = "node-instance-connection" },
138 { .id = ACLK_TOPICID_CHART_DIMS, .name = "chart-and-dims-updated" },
139 { .id = ACLK_TOPICID_CHART_CONFIGS_UPDATED, .name = "chart-configs-updated" },
140 { .id = ACLK_TOPICID_CHART_RESET, .name = "reset-charts" },
141 { .id = ACLK_TOPICID_RETENTION_UPDATED, .name = "chart-retention-updated" },
142 { .id = ACLK_TOPICID_NODE_INFO, .name = "node-instance-info" },
143 { .id = ACLK_TOPICID_ALARM_LOG, .name = "alarm-log-v2" },
144 { .id = ACLK_TOPICID_ALARM_CHECKPOINT, .name = "alarm-checkpoint" },
145 { .id = ACLK_TOPICID_ALARM_CONFIG, .name = "alarm-config" },
146 { .id = ACLK_TOPICID_ALARM_SNAPSHOT, .name = "alarm-snapshot-v2" },
147 { .id = ACLK_TOPICID_NODE_COLLECTORS, .name = "node-instance-collectors" },
148 { .id = ACLK_TOPICID_CTXS_SNAPSHOT, .name = "contexts-snapshot" },
149 { .id = ACLK_TOPICID_CTXS_UPDATED, .name = "contexts-updated" },
150 { .id = ACLK_TOPICID_UNKNOWN, .name = NULL }
151 };
152
153 enum aclk_topics compulsory_topics[] = {
154 // TODO remove old topics once not needed anymore
155 ACLK_TOPICID_CHART, //TODO from legacy
156 ACLK_TOPICID_ALARMS, //TODO from legacy
157 ACLK_TOPICID_METADATA, //TODO from legacy
158 ACLK_TOPICID_COMMAND,
159 ACLK_TOPICID_AGENT_CONN,
160 ACLK_TOPICID_CMD_NG_V1,
161 ACLK_TOPICID_CREATE_NODE,
162 ACLK_TOPICID_NODE_CONN,
163 ACLK_TOPICID_CHART_DIMS,
164 ACLK_TOPICID_CHART_CONFIGS_UPDATED,
165 ACLK_TOPICID_CHART_RESET,
166 ACLK_TOPICID_RETENTION_UPDATED,
167 ACLK_TOPICID_NODE_INFO,
168 ACLK_TOPICID_ALARM_LOG,
169 ACLK_TOPICID_ALARM_CHECKPOINT,
170 ACLK_TOPICID_ALARM_CONFIG,
171 ACLK_TOPICID_ALARM_SNAPSHOT,
172 ACLK_TOPICID_NODE_COLLECTORS,
173 ACLK_TOPICID_CTXS_SNAPSHOT,
174 ACLK_TOPICID_CTXS_UPDATED,
175 ACLK_TOPICID_UNKNOWN
176 };
177
178 static enum aclk_topics topic_name_to_id(const char *name) {
179 struct topic_name *topic = topic_names;
180 while (topic->name) {
181 if (!strcmp(topic->name, name)) {
182 return topic->id;
183 }
184 topic++;
185 }
186 return ACLK_TOPICID_UNKNOWN;
187 }
188
189 static const char *topic_id_to_name(enum aclk_topics tid) {
190 struct topic_name *topic = topic_names;
191 while (topic->name) {
192 if (topic->id == tid)
193 return topic->name;
194 topic++;
195 }
196 return "unknown";
197 }
198
199 #define CLAIM_ID_REPLACE_TAG "#{claim_id}"
200 static void topic_generate_final(struct aclk_topic *t) {
201 char *dest;
202 char *replace_tag = strstr(t->topic_recvd, CLAIM_ID_REPLACE_TAG);
203 if (!replace_tag)
204 return;
205
206 CLAIM_ID claim_id = claim_id_get();
207 if (unlikely(!claim_id_is_set(claim_id))) {
208 netdata_log_error("This should never be called if agent not claimed");
209 return;
210 }
211
212 t->topic = mallocz(strlen(t->topic_recvd) + 1 - strlen(CLAIM_ID_REPLACE_TAG) + strlen(claim_id.str));
213 memcpy(t->topic, t->topic_recvd, replace_tag - t->topic_recvd);
214 dest = t->topic + (replace_tag - t->topic_recvd);
215
216 memcpy(dest, claim_id.str, strlen(claim_id.str));
217 dest += strlen(claim_id.str);
218 replace_tag += strlen(CLAIM_ID_REPLACE_TAG);
219 strcpy(dest, replace_tag);
220 dest += strlen(replace_tag);
221 *dest = 0;
222 }
223
224 static int topic_cache_add_topic(struct json_object *json, struct aclk_topic *topic)
225 {
226 struct json_object_iterator it;
227 struct json_object_iterator itEnd;
228
229 it = json_object_iter_begin(json);
230 itEnd = json_object_iter_end(json);
231
232 while (!json_object_iter_equal(&it, &itEnd)) {
233 if (!strcmp(json_object_iter_peek_name(&it), JSON_TOPIC_KEY_NAME)) {
234 if (json_object_get_type(json_object_iter_peek_value(&it)) != json_type_string) {
235 netdata_log_error("topic dictionary key \"" JSON_TOPIC_KEY_NAME "\" is expected to be json_type_string");
236 return 1;
237 }
238 topic->topic_id = topic_name_to_id(json_object_get_string(json_object_iter_peek_value(&it)));
239 if (topic->topic_id == ACLK_TOPICID_UNKNOWN) {
240 netdata_log_debug(D_ACLK, "topic dictionary has unknown topic name \"%s\"", json_object_get_string(json_object_iter_peek_value(&it)));
241 }
242 json_object_iter_next(&it);
243 continue;
244 }
245 if (!strcmp(json_object_iter_peek_name(&it), JSON_TOPIC_KEY_TOPIC)) {
246 if (json_object_get_type(json_object_iter_peek_value(&it)) != json_type_string) {
247 netdata_log_error("topic dictionary key \"" JSON_TOPIC_KEY_TOPIC "\" is expected to be json_type_string");
248 return 1;
249 }
250 topic->topic_recvd = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
251 json_object_iter_next(&it);
252 continue;
253 }
254
255 netdata_log_error("topic dictionary has Unknown/Unexpected key \"%s\" in topic description. Ignoring!", json_object_iter_peek_name(&it));
256 json_object_iter_next(&it);
257 }
258
259 if (!topic->topic_recvd) {
260 netdata_log_error("topic dictionary Missig compulsory key %s", JSON_TOPIC_KEY_TOPIC);
261 return 1;
262 }
263
264 topic_generate_final(topic);
265 aclk_topic_cache_items++;
266
267 return 0;
268 }
269
270 int aclk_generate_topic_cache(struct json_object *json)
271 {
272 json_object *obj;
273
274 size_t array_size = json_object_array_length(json);
275 if (!array_size) {
276 netdata_log_error("Empty topic list!");
277 return 1;
278 }
279
280 if (aclk_topic_cache)
281 free_topic_cache();
282
283 aclk_topic_cache = callocz(array_size, sizeof(struct aclk_topic *));
284
285 for (size_t i = 0; i < array_size; i++) {
286 obj = json_object_array_get_idx(json, i);
287 if (json_object_get_type(obj) != json_type_object) {
288 netdata_log_error("expected json_type_object");
289 return 1;
290 }
291 aclk_topic_cache[i] = callocz(1, sizeof(struct aclk_topic));
292 if (topic_cache_add_topic(obj, aclk_topic_cache[i])) {
293 netdata_log_error("failed to parse topic @idx=%d", (int)i);
294 return 1;
295 }
296 }
297
298 for (int i = 0; compulsory_topics[i] != ACLK_TOPICID_UNKNOWN; i++) {
299 if (!aclk_get_topic(compulsory_topics[i])) {
300 netdata_log_error("missing compulsory topic \"%s\" in password response from cloud", topic_id_to_name(compulsory_topics[i]));
301 return 1;
302 }
303 }
304
305 return 0;
306 }
307
308 /*
309 * Build a topic based on sub_topic and final_topic
310 * if the sub topic starts with / assume that is an absolute topic
311 *
312 */
313 const char *aclk_get_topic(enum aclk_topics topic)
314 {
315 if (!aclk_topic_cache) {
316 netdata_log_error("Topic cache not initialized");
317 return NULL;
318 }
319
320 for (size_t i = 0; i < aclk_topic_cache_items; i++) {
321 if (aclk_topic_cache[i]->topic_id == topic)
322 return aclk_topic_cache[i]->topic;
323 }
324 netdata_log_error("Unknown topic");
325 return NULL;
326 }
327
328 /*
329 * Allows iterating all topics in topic cache without
330 * having to resort to callbacks.
331 */
332
333 const char *aclk_topic_cache_iterate(size_t *iter)
334 {
335 if (!aclk_topic_cache) {
336 netdata_log_error("Topic cache not initialized when %s was called.", __FUNCTION__);
337 return NULL;
338 }
339
340 if (*iter >= aclk_topic_cache_items)
341 return NULL;
342
343 return aclk_topic_cache[(*iter)++]->topic;
344 }
345
346 /*
347 * TBEB with randomness
348 *
349 * @param reset 1 - to reset the delay,
350 * 0 - to advance a step and calculate sleep time in ms
351 * @param min, max in seconds
352 * @returns delay in ms
353 *
354 */
355
356 unsigned long int aclk_tbeb_delay(int reset, int base, unsigned long int mins_ms, unsigned long int min_ms) {
357 static int attempt = -1;
358
359 if (reset) {
360 attempt = -1;
361 return 0;
362 }
363
364 attempt++;
365
366 if (attempt == 0)
367 return 0;
368
369 unsigned long int delay = pow(base, attempt - 1);
370 delay *= MSEC_PER_SEC;
371
372 delay += (os_random32() % (MAX(1000, delay/2)));
373
374 // Note: this is a bug, the value expected from the env backoff payload should be in seconds
375 // but the code here is in milliseconds. To avoid confusion the cloud will be sending the value
376 // in milliseconds so that the code will work as expected.
377 if (delay <= mins_ms * MSEC_PER_SEC)
378 return mins_ms;
379
380 if (delay >= min_ms * MSEC_PER_SEC)
381 return min_ms;
382
383 return delay;
384 }
385
386 static inline int aclk_parse_userpass_pair(const char *src, const char c, char **a, char **b)
387 {
388 const char *ptr = strchr(src, c);
389 if (ptr == NULL)
390 return 1;
391
392 char *tmp_a = callocz(1, ptr - src + 1);
393 memcpy(tmp_a, src, ptr - src);
394
395 char *decoded_a = callocz(1, ptr - src + 1);
396 url_decode_r(decoded_a, tmp_a, ptr - src + 1);
397 freez(tmp_a);
398 *a = decoded_a;
399
400 char *tmp_b = strdupz(ptr+1);
401 char *decoded_b = callocz(1, strlen(tmp_b) + 1);
402 url_decode_r(decoded_b, tmp_b, strlen(tmp_b) + 1);
403 freez(tmp_b);
404 *b = decoded_b;
405
406 return 0;
407 }
408
409 #define HTTP_PROXY_PREFIX "http://"
410 #define SOCKS5_PROXY_PREFIX "socks5://"
411 #define SOCKS5H_PROXY_PREFIX "socks5h://"
412 void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd,
413 char **log_proxy, enum mqtt_wss_proxy_type *type)
414 {
415 ACLK_PROXY_TYPE pt;
416 const char *ptr = aclk_get_proxy(&pt, false);
417 *log_proxy = (char *) aclk_get_proxy(&pt, true);
418 char *tmp;
419 const char *prefix = NULL;
420 int default_port = 0;
421
422 if (pt != PROXY_TYPE_HTTP && pt != PROXY_TYPE_SOCKS5 && pt != PROXY_TYPE_SOCKS5H)
423 return;
424
425 *uname = NULL;
426 *pwd = NULL;
427 *port = 0;
428
429 char *proxy = strdupz(ptr);
430 ptr = proxy;
431
432 switch (pt) {
433 case PROXY_TYPE_HTTP:
434 prefix = HTTP_PROXY_PREFIX;
435 default_port = 8080;
436 if (type)
437 *type = MQTT_WSS_PROXY_HTTP;
438 break;
439 case PROXY_TYPE_SOCKS5:
440 prefix = SOCKS5_PROXY_PREFIX;
441 default_port = 1080;
442 if (type)
443 *type = MQTT_WSS_PROXY_SOCKS5;
444 break;
445 case PROXY_TYPE_SOCKS5H:
446 prefix = SOCKS5H_PROXY_PREFIX;
447 default_port = 1080;
448 if (type)
449 *type = MQTT_WSS_PROXY_SOCKS5H;
450 break;
451 default:
452 aclk_sensitive_free(&proxy);
453 return;
454 }
455
456 if (!strncmp(ptr, prefix, strlen(prefix)))
457 ptr += strlen(prefix);
458
459 if ((tmp = strchr(ptr, '@'))) {
460 *tmp = 0;
461 if(aclk_parse_userpass_pair(ptr, ':', uname, pwd)) {
462 error_report("Failed to get username and password for proxy. Will attempt connection without authentication");
463 }
464 ptr = tmp+1;
465 }
466
467 if (!*ptr) {
468 aclk_sensitive_free(&proxy);
469 aclk_sensitive_free(uname);
470 aclk_sensitive_free(pwd);
471 return;
472 }
473
474 if ((tmp = strchr(ptr, ':'))) {
475 *tmp = 0;
476 tmp++;
477 if(*tmp)
478 *port = atoi(tmp);
479 }
480 *ohost = strdupz(ptr);
481
482 if (*port <= 0 || *port > 65535)
483 *port = default_port;
484
485 if (!type) {
486 aclk_sensitive_free(uname);
487 aclk_sensitive_free(pwd);
488 }
489
490 aclk_sensitive_free(&proxy);
491 }
492
493 enum mqtt_wss_proxy_type aclk_proxy_type_from_scheme(const char *proxy_url)
494 {
495 if (!proxy_url || !*proxy_url)
496 return MQTT_WSS_DIRECT;
497
498 if (!strncmp(proxy_url, SOCKS5H_PROXY_PREFIX, strlen(SOCKS5H_PROXY_PREFIX)))
499 return MQTT_WSS_PROXY_SOCKS5H;
500
501 if (!strncmp(proxy_url, SOCKS5_PROXY_PREFIX, strlen(SOCKS5_PROXY_PREFIX)))
502 return MQTT_WSS_PROXY_SOCKS5;
503
504 if (!strncmp(proxy_url, HTTP_PROXY_PREFIX, strlen(HTTP_PROXY_PREFIX)))
505 return MQTT_WSS_PROXY_HTTP;
506
507 return MQTT_WSS_DIRECT;
508 }
509
510 const char *aclk_mqtt_proxy_type_to_scheme(enum mqtt_wss_proxy_type type)
511 {
512 switch (type) {
513 case MQTT_WSS_PROXY_HTTP:
514 return HTTP_PROXY_PREFIX;
515 case MQTT_WSS_PROXY_SOCKS5:
516 return SOCKS5_PROXY_PREFIX;
517 case MQTT_WSS_PROXY_SOCKS5H:
518 return SOCKS5H_PROXY_PREFIX;
519 default:
520 return "";
521 }
522 }
523
524 static int aclk_poll_for_io(int fd, short events, int timeout_ms)
525 {
526 struct pollfd pfd = {
527 .fd = fd,
528 .events = events,
529 .revents = 0
530 };
531
532 int rc;
533 do {
534 rc = poll(&pfd, 1, timeout_ms);
535 } while (rc < 0 && errno == EINTR);
536
537 if (rc <= 0)
538 return rc;
539
540 if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0)
541 return -1;
542
543 return 1;
544 }
545
546 static int aclk_timeout_remaining_ms(usec_t start, int timeout_ms)
547 {
548 if (timeout_ms <= 0)
549 return 0;
550
551 usec_t elapsed_ms = (now_monotonic_usec() - start) / USEC_PER_MS;
552 if (elapsed_ms >= (usec_t)timeout_ms)
553 return 0;
554
555 return timeout_ms - (int)elapsed_ms;
556 }
557
558 static int aclk_write_all_timeout(int fd, const void *buf, size_t len, int timeout_ms)
559 {
560 size_t written = 0;
561 usec_t start = now_monotonic_usec();
562
563 while (written < len) {
564 int remaining_ms = aclk_timeout_remaining_ms(start, timeout_ms);
565 if (remaining_ms <= 0)
566 return 1;
567
568 int rc = aclk_poll_for_io(fd, POLLOUT, remaining_ms);
569 if (rc <= 0)
570 return 1;
571
572 ssize_t n = write(fd, ((const uint8_t *)buf) + written, len - written);
573 if (n < 0) {
574 if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
575 if ((now_monotonic_usec() - start) / USEC_PER_MS > (usec_t)timeout_ms)
576 return 1;
577 continue;
578 }
579 return 1;
580 }
581 written += (size_t)n;
582 }
583
584 return 0;
585 }
586
587 static int aclk_read_exact_timeout(int fd, void *buf, size_t len, int timeout_ms)
588 {
589 size_t got = 0;
590 usec_t start = now_monotonic_usec();
591
592 while (got < len) {
593 int remaining_ms = aclk_timeout_remaining_ms(start, timeout_ms);
594 if (remaining_ms <= 0)
595 return 1;
596
597 int rc = aclk_poll_for_io(fd, POLLIN, remaining_ms);
598 if (rc <= 0)
599 return 1;
600
601 ssize_t n = read(fd, ((uint8_t *)buf) + got, len - got);
602 if (n == 0)
603 return 1;
604 if (n < 0) {
605 if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
606 if ((now_monotonic_usec() - start) / USEC_PER_MS > (usec_t)timeout_ms)
607 return 1;
608 continue;
609 }
610 return 1;
611 }
612 got += (size_t)n;
613 }
614
615 return 0;
616 }
617
618 static int aclk_http_proxy_negotiate(int sockfd, const char *proxy_username, const char *proxy_password,
619 const char *target_host, int target_port, int timeout_ms)
620 {
621 int result = 1;
622 bool has_creds = (proxy_username && *proxy_username);
623 usec_t start = now_monotonic_usec();
624 char req[4096];
625 size_t off = 0;
626 int rc = snprintf(req + off, sizeof(req) - off, "CONNECT %s:%d HTTP/1.1\r\nHost: %s\r\n",
627 target_host, target_port, target_host);
628 if (rc < 0 || (size_t)rc >= sizeof(req) - off)
629 goto cleanup;
630 off += (size_t)rc;
631
632 if (has_creds) {
633 size_t pass_len = proxy_password ? strlen(proxy_password) : 0;
634 size_t creds_plain_len = strlen(proxy_username) + pass_len + 1;
635 char *creds_plain = callocz(1, creds_plain_len + 1);
636 snprintfz(creds_plain, creds_plain_len + 1, "%s:%s", proxy_username, proxy_password ? proxy_password : "");
637
638 int creds_base64_len = (((4 * (int)creds_plain_len / 3) + 3) & ~3);
639 creds_base64_len += (1 + (creds_base64_len / 64)) * (int)strlen("\n");
640 char *creds_base64 = callocz(1, (size_t)creds_base64_len + 1);
641 (void)netdata_base64_encode((unsigned char *)creds_base64, (unsigned char *)creds_plain, creds_plain_len);
642
643 rc = snprintf(req + off, sizeof(req) - off, "Proxy-Authorization: Basic %s\r\n", creds_base64);
644 aclk_sensitive_free(&creds_plain);
645 aclk_sensitive_free(&creds_base64);
646 if (rc < 0 || (size_t)rc >= sizeof(req) - off)
647 goto cleanup;
648 off += (size_t)rc;
649 }
650
651 if (off + 2 >= sizeof(req))
652 goto cleanup;
653 req[off++] = '\r';
654 req[off++] = '\n';
655
656 if (aclk_write_all_timeout(sockfd, req, off, aclk_timeout_remaining_ms(start, timeout_ms)))
657 goto cleanup;
658
659 // Read the HTTP response one byte at a time to avoid over-reading
660 // beyond the header terminator into TLS handshake data.
661 char resp[4096];
662 size_t used = 0;
663 while (used < sizeof(resp) - 1) {
664 int remaining_ms = aclk_timeout_remaining_ms(start, timeout_ms);
665 if (remaining_ms <= 0)
666 goto cleanup;
667
668 int prc = aclk_poll_for_io(sockfd, POLLIN, remaining_ms);
669 if (prc <= 0)
670 goto cleanup;
671
672 ssize_t n = read(sockfd, resp + used, 1);
673 if (n == 0)
674 goto cleanup;
675 if (n < 0) {
676 if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
677 continue;
678 goto cleanup;
679 }
680 used += (size_t)n;
681 resp[used] = '\0';
682
683 // Check if we have received the complete header terminator
684 if (used >= 4 && memcmp(resp + used - 4, "\r\n\r\n", 4) == 0)
685 break;
686 }
687
688 if (used < 4 || memcmp(resp + used - 4, "\r\n\r\n", 4) != 0)
689 goto cleanup;
690
691 if (strncmp(resp, "HTTP/1.1 ", 9) != 0 && strncmp(resp, "HTTP/1.0 ", 9) != 0)
692 goto cleanup;
693
694 if (!isdigit((unsigned char)resp[9]) || !isdigit((unsigned char)resp[10]) || !isdigit((unsigned char)resp[11])) {
695 netdata_log_error("ACLK: HTTP proxy response missing valid status code");
696 goto cleanup;
697 }
698
699 int status = atoi(resp + 9);
700 if (status == 200) {
701 result = 0;
702 } else {
703 // extract the status line (first line) for logging
704 char *eol = strstr(resp, "\r\n");
705 if (eol)
706 *eol = '\0';
707 netdata_log_error("ACLK: HTTP proxy CONNECT to %s:%d failed with status %d: %s",
708 target_host, target_port, status, resp);
709 result = 1;
710 }
711
712 cleanup:
713 if (has_creds)
714 aclk_sensitive_memzero(req, sizeof(req));
715 return result;
716 }
717
718 static int aclk_socks5_resolve_local(const char *host, uint8_t *atype, uint8_t *addr, size_t *addr_len)
719 {
720 struct in_addr ipv4;
721 struct in6_addr ipv6;
722 if (inet_pton(AF_INET, host, &ipv4) == 1) {
723 *atype = 0x01;
724 memcpy(addr, &ipv4, sizeof(ipv4));
725 *addr_len = sizeof(ipv4);
726 return 0;
727 }
728 if (inet_pton(AF_INET6, host, &ipv6) == 1) {
729 *atype = 0x04;
730 memcpy(addr, &ipv6, sizeof(ipv6));
731 *addr_len = sizeof(ipv6);
732 return 0;
733 }
734
735 struct addrinfo hints = {
736 .ai_family = AF_UNSPEC,
737 .ai_socktype = SOCK_STREAM,
738 .ai_flags = AI_ADDRCONFIG
739 };
740 struct addrinfo *res = NULL;
741 if (getaddrinfo(host, NULL, &hints, &res) != 0 || !res)
742 return 1;
743
744 int rc = 1;
745
746 // Prefer IPv4 for compatibility with SOCKS proxies that don't accept ATYP=0x04 (IPv6).
747 for (struct addrinfo *it = res; it; it = it->ai_next) {
748 if (it->ai_family == AF_INET && it->ai_addrlen >= sizeof(struct sockaddr_in)) {
749 struct sockaddr_in *sa = (struct sockaddr_in *)it->ai_addr;
750 *atype = 0x01;
751 memcpy(addr, &sa->sin_addr, sizeof(sa->sin_addr));
752 *addr_len = sizeof(sa->sin_addr);
753 rc = 0;
754 break;
755 }
756 }
757
758 if (rc != 0) {
759 for (struct addrinfo *it = res; it; it = it->ai_next) {
760 if (it->ai_family == AF_INET6 && it->ai_addrlen >= sizeof(struct sockaddr_in6)) {
761 struct sockaddr_in6 *sa = (struct sockaddr_in6 *)it->ai_addr;
762 *atype = 0x04;
763 memcpy(addr, &sa->sin6_addr, sizeof(sa->sin6_addr));
764 *addr_len = sizeof(sa->sin6_addr);
765 rc = 0;
766 break;
767 }
768 }
769 }
770
771 freeaddrinfo(res);
772 return rc;
773 }
774
775 static int aclk_socks5_proxy_negotiate(int sockfd, enum mqtt_wss_proxy_type proxy_type,
776 const char *proxy_username, const char *proxy_password,
777 const char *target_host, int target_port, int timeout_ms)
778 {
779 usec_t start = now_monotonic_usec();
780
781 uint8_t greeting[4] = { 0x05, 0x01, 0x00, 0x00 };
782 size_t greeting_len = 3;
783 if (proxy_username && *proxy_username) {
784 greeting[1] = 0x02;
785 greeting[2] = 0x00;
786 greeting[3] = 0x02;
787 greeting_len = 4;
788 }
789
790 if (aclk_write_all_timeout(sockfd, greeting, greeting_len, aclk_timeout_remaining_ms(start, timeout_ms))) {
791 return 1;
792 }
793
794 uint8_t greeting_reply[2];
795 if (aclk_read_exact_timeout(sockfd, greeting_reply, sizeof(greeting_reply), aclk_timeout_remaining_ms(start, timeout_ms))) {
796 return 1;
797 }
798 if (greeting_reply[0] != 0x05 || greeting_reply[1] == 0xFF) {
799 netdata_log_error("ACLK: SOCKS5 proxy rejected methods (ver=0x%02x method=0x%02x)",
800 greeting_reply[0], greeting_reply[1]);
801 return 1;
802 }
803
804 if (greeting_reply[1] == 0x02) {
805 if (!proxy_username || !*proxy_username) {
806 netdata_log_error("ACLK: SOCKS5 proxy requires username/password auth but credentials are missing");
807 return 1;
808 }
809 size_t user_len = strlen(proxy_username);
810 size_t pass_len = proxy_password ? strlen(proxy_password) : 0;
811 if (user_len > UINT8_MAX || pass_len > UINT8_MAX) {
812 netdata_log_error("ACLK: SOCKS5 credentials exceed protocol limits");
813 return 1;
814 }
815
816 uint8_t auth_req[513];
817 size_t pos = 0;
818 auth_req[pos++] = 0x01;
819 auth_req[pos++] = (uint8_t)user_len;
820 memcpy(auth_req + pos, proxy_username, user_len);
821 pos += user_len;
822 auth_req[pos++] = (uint8_t)pass_len;
823 if (pass_len) {
824 memcpy(auth_req + pos, proxy_password, pass_len);
825 pos += pass_len;
826 }
827
828 int auth_rc = aclk_write_all_timeout(sockfd, auth_req, pos, aclk_timeout_remaining_ms(start, timeout_ms));
829 aclk_sensitive_memzero(auth_req, sizeof(auth_req));
830 if (auth_rc) {
831 return 1;
832 }
833
834 uint8_t auth_reply[2];
835 if (aclk_read_exact_timeout(sockfd, auth_reply, sizeof(auth_reply), aclk_timeout_remaining_ms(start, timeout_ms))) {
836 return 1;
837 }
838 if (auth_reply[0] != 0x01 || auth_reply[1] != 0x00) {
839 netdata_log_error("ACLK: SOCKS5 auth failed (ver=0x%02x status=0x%02x)",
840 auth_reply[0], auth_reply[1]);
841 return 1;
842 }
843 } else if (greeting_reply[1] != 0x00) {
844 netdata_log_error("ACLK: SOCKS5 proxy selected unsupported method 0x%02x", greeting_reply[1]);
845 return 1;
846 }
847
848 uint8_t connect_req[300];
849 size_t pos = 0;
850 connect_req[pos++] = 0x05;
851 connect_req[pos++] = 0x01;
852 connect_req[pos++] = 0x00;
853
854 if (proxy_type == MQTT_WSS_PROXY_SOCKS5H) {
855 size_t host_len = strlen(target_host);
856 if (host_len == 0 || host_len > UINT8_MAX) {
857 netdata_log_error("ACLK: SOCKS5H target hostname length invalid (%zu)", host_len);
858 return 1;
859 }
860 // 1 (atype) + 1 (length) + host_len + 2 (port)
861 if (pos + 1 + 1 + host_len + 2 > sizeof(connect_req)) {
862 netdata_log_error("ACLK: SOCKS5H CONNECT request too large for buffer");
863 return 1;
864 }
865 connect_req[pos++] = 0x03;
866 connect_req[pos++] = (uint8_t)host_len;
867 memcpy(connect_req + pos, target_host, host_len);
868 pos += host_len;
869 } else {
870 uint8_t atyp = 0;
871 uint8_t addr[16];
872 size_t addr_len = 0;
873 if (aclk_socks5_resolve_local(target_host, &atyp, addr, &addr_len)) {
874 netdata_log_error("ACLK: SOCKS5 local DNS resolution failed for target host '%s'", target_host);
875 return 1;
876 }
877 // 1 (atype) + addr_len + 2 (port)
878 if (pos + 1 + addr_len + 2 > sizeof(connect_req)) {
879 netdata_log_error("ACLK: SOCKS5 CONNECT request too large for buffer");
880 return 1;
881 }
882 connect_req[pos++] = atyp;
883 memcpy(connect_req + pos, addr, addr_len);
884 pos += addr_len;
885 }
886
887 connect_req[pos++] = (uint8_t)((target_port >> 8) & 0xFF);
888 connect_req[pos++] = (uint8_t)(target_port & 0xFF);
889
890 if (aclk_write_all_timeout(sockfd, connect_req, pos, aclk_timeout_remaining_ms(start, timeout_ms))) {
891 return 1;
892 }
893
894 uint8_t reply_hdr[4];
895 if (aclk_read_exact_timeout(sockfd, reply_hdr, sizeof(reply_hdr), aclk_timeout_remaining_ms(start, timeout_ms))) {
896 return 1;
897 }
898 if (reply_hdr[0] != 0x05 || reply_hdr[1] != 0x00) {
899 netdata_log_error("ACLK: SOCKS5 CONNECT failed (ver=0x%02x rep=0x%02x atyp=0x%02x)",
900 reply_hdr[0], reply_hdr[1], reply_hdr[3]);
901 return 1;
902 }
903
904 size_t to_read = 0;
905 switch (reply_hdr[3]) {
906 case 0x01:
907 to_read = 4 + 2;
908 break;
909 case 0x03: {
910 uint8_t domain_len = 0;
911 if (aclk_read_exact_timeout(sockfd, &domain_len, 1, aclk_timeout_remaining_ms(start, timeout_ms))) {
912 return 1;
913 }
914 to_read = (size_t)domain_len + 2;
915 break;
916 }
917 case 0x04:
918 to_read = 16 + 2;
919 break;
920 default:
921 netdata_log_error("ACLK: SOCKS5 CONNECT reply has invalid ATYP 0x%02x", reply_hdr[3]);
922 return 1;
923 }
924
925 uint8_t discard[260];
926 if (to_read > sizeof(discard)) {
927 return 1;
928 }
929
930 if (aclk_read_exact_timeout(sockfd, discard, to_read, aclk_timeout_remaining_ms(start, timeout_ms))) {
931 return 1;
932 }
933
934 return 0;
935 }
936
937 int aclk_proxy_negotiation_connect(int sockfd, enum mqtt_wss_proxy_type proxy_type,
938 const char *proxy_username, const char *proxy_password,
939 const char *target_host, int target_port, int timeout_ms)
940 {
941 if (proxy_type == MQTT_WSS_DIRECT)
942 return 0;
943
944 if (proxy_type == MQTT_WSS_PROXY_HTTP)
945 return aclk_http_proxy_negotiate(sockfd, proxy_username, proxy_password, target_host, target_port, timeout_ms);
946
947 if (proxy_type == MQTT_WSS_PROXY_SOCKS5 || proxy_type == MQTT_WSS_PROXY_SOCKS5H)
948 return aclk_socks5_proxy_negotiate(sockfd, proxy_type, proxy_username, proxy_password, target_host, target_port, timeout_ms);
949
950 return 1;
951 }