master
c 588 lines 20.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "claim.h"
4
5 #include "registry/registry.h"
6
7 #include <curl/curl.h>
8 #include <openssl/evp.h>
9 #include <openssl/pem.h>
10 #include <openssl/err.h>
11
12 #define CLAIM_RESPONSE_SIZE_LIMIT (10 * 1024 * 1024)
13
14 struct claim_response_buffer {
15 BUFFER *wb;
16 bool too_large;
17 };
18
19 static bool check_and_generate_certificates() {
20 FILE *fp;
21 EVP_PKEY *pkey = NULL;
22 EVP_PKEY_CTX *pctx = NULL;
23
24 CLEAN_CHAR_P *private_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "private.pem");
25 CLEAN_CHAR_P *public_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "public.pem");
26
27 // Check if private key exists
28 fp = fopen(public_key_file, "r");
29 if (fp) {
30 fclose(fp);
31 return true;
32 }
33
34 // Generate the RSA key
35 pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
36 if (!pctx) {
37 claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_CTX_new_id() failed");
38 return false;
39 }
40
41 if (EVP_PKEY_keygen_init(pctx) <= 0) {
42 claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_keygen_init() failed");
43 EVP_PKEY_CTX_free(pctx);
44 return false;
45 }
46
47 if (EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0) {
48 claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_CTX_set_rsa_keygen_bits() failed");
49 EVP_PKEY_CTX_free(pctx);
50 return false;
51 }
52
53 if (EVP_PKEY_keygen(pctx, &pkey) <= 0) {
54 claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_keygen() failed");
55 EVP_PKEY_CTX_free(pctx);
56 return false;
57 }
58
59 EVP_PKEY_CTX_free(pctx);
60
61 // Save private key
62 fp = fopen(private_key_file, "wb");
63 if (!fp || !PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL)) {
64 claim_agent_failure_reason_set("Cannot write private key file: %s", private_key_file);
65 if (fp) fclose(fp);
66 EVP_PKEY_free(pkey);
67 return false;
68 }
69 fclose(fp);
70
71 // Save public key
72 fp = fopen(public_key_file, "wb");
73 if (!fp || !PEM_write_PUBKEY(fp, pkey)) {
74 claim_agent_failure_reason_set("Cannot write public key file: %s", public_key_file);
75 if (fp) fclose(fp);
76 EVP_PKEY_free(pkey);
77 return false;
78 }
79 fclose(fp);
80
81 EVP_PKEY_free(pkey);
82 return true;
83 }
84
85 static size_t response_write_callback(void *ptr, size_t size, size_t nmemb, void *stream) {
86 struct claim_response_buffer *response = stream;
87
88 if (unlikely(nmemb && size > SIZE_MAX / nmemb)) {
89 response->too_large = true;
90 return 0;
91 }
92 size_t real_size = size * nmemb;
93
94 if (unlikely(real_size > CLAIM_RESPONSE_SIZE_LIMIT - buffer_strlen(response->wb))) {
95 response->too_large = true;
96 return 0;
97 }
98
99 buffer_memcat(response->wb, ptr, real_size);
100
101 return real_size;
102 }
103
104 static const char *curl_add_json_room(BUFFER *wb, const char *start, const char *end, bool last_item)
105 {
106 size_t len = end - start;
107
108 // copy the item to an new buffer and terminate it
109 char *buf = mallocz(len + 1);
110 memcpy(buf, start, len);
111 buf[len] = '\0';
112
113 // add it to the json array
114 const char *trimmed = trim(buf); // remove leading and trailing spaces
115 if(trimmed)
116 buffer_json_add_array_item_string(wb, trimmed);
117
118 freez(buf);
119
120 if (last_item)
121 return NULL;
122
123 // prepare for the next item
124 start = end + 1;
125
126 // skip multiple separators or spaces
127 while(*start == ',' || *start == ' ') start++;
128
129 return start;
130 }
131
132 void curl_add_rooms_json_array(BUFFER *wb, const char *rooms) {
133 buffer_json_member_add_array(wb, "rooms");
134 if(rooms && *rooms) {
135 const char *start = rooms, *end = NULL;
136
137 // Skip initial separators or spaces
138 while (*start == ',' || *start == ' ')
139 start++;
140
141 // Process each item in the comma-separated list
142 while ((end = strchr(start, ',')) != NULL)
143 start = curl_add_json_room(wb, start, end, false);
144
145 // Process the last item if any
146 if (*start)
147 curl_add_json_room(wb, start, &start[strlen(start)], true);
148 }
149 buffer_json_array_close(wb);
150 }
151
152 static int debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr) {
153 (void)handle; // Unused
154 (void)userptr; // Unused
155
156 if (type == CURLINFO_TEXT)
157 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Info: %s", data);
158 else if (type == CURLINFO_HEADER_OUT)
159 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send header: %.*s", (int)size, data);
160 else if (type == CURLINFO_DATA_OUT)
161 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send data: %.*s", (int)size, data);
162 else if (type == CURLINFO_SSL_DATA_OUT)
163 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send SSL data: %.*s", (int)size, data);
164 else if (type == CURLINFO_HEADER_IN)
165 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive header: %.*s", (int)size, data);
166 else if (type == CURLINFO_DATA_IN)
167 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive data: %.*s", (int)size, data);
168 else if (type == CURLINFO_SSL_DATA_IN)
169 nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive SSL data: %.*s", (int)size, data);
170
171 return 0;
172 }
173
174 static bool cleanup_curl_request_failure(CURL *curl, struct curl_slist *headers, bool *can_retry, CURLcode res,
175 const char *option) {
176 claim_agent_failure_reason_set("Cannot configure request (%s failed: %s)", option, curl_easy_strerror(res));
177
178 curl_easy_cleanup(curl);
179 if(headers)
180 curl_slist_free_all(headers);
181
182 *can_retry = false;
183 return false;
184 }
185
186 static bool send_curl_request(const char *machine_guid, const char *hostname, const char *token, const char *rooms, const char *url, const char *proxy, bool insecure, bool *can_retry) {
187 CURL *curl;
188 CURLcode res;
189 char target_url[2048];
190 char public_key[2048] = "";
191 size_t public_key_bytes_read = 0;
192 FILE *fp;
193 struct curl_slist *headers = NULL;
194
195 // create a new random claim id
196 nd_uuid_t claimed_id;
197 uuid_generate_random(claimed_id);
198 char claimed_id_str[UUID_STR_LEN];
199 uuid_unparse_lower(claimed_id, claimed_id_str);
200
201 // generate the URL to post
202 snprintf(target_url, sizeof(target_url), "%s%sapi/v1/spaces/nodes/%s",
203 url, strendswith(url, "/") ? "" : "/", claimed_id_str);
204
205 // Read the public key
206 CLEAN_CHAR_P *public_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "public.pem");
207 fp = fopen(public_key_file, "r");
208 if (!fp || (public_key_bytes_read = fread(public_key, 1, sizeof(public_key) - 1, fp)) == 0) {
209 claim_agent_failure_reason_set("cannot read public key file '%s'", public_key_file);
210 if (fp) fclose(fp);
211 *can_retry = false;
212 return false;
213 }
214 public_key[public_key_bytes_read] = '\0';
215 fclose(fp);
216
217 // check if we have trusted.pem
218 // or cloud_fullchain.pem, for backwards compatibility
219 CLEAN_CHAR_P *trusted_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "trusted.pem");
220 fp = fopen(trusted_key_file, "r");
221 if(fp)
222 fclose(fp);
223 else {
224 freez(trusted_key_file);
225 trusted_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "cloud_fullchain.pem");
226 fp = fopen(trusted_key_file, "r");
227 if(fp)
228 fclose(fp);
229 else {
230 freez(trusted_key_file);
231 trusted_key_file = NULL;
232 }
233 }
234
235 // generate the JSON request message
236 CLEAN_BUFFER *wb = buffer_create(0, NULL);
237 buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
238
239 buffer_json_member_add_object(wb, "node");
240 {
241 buffer_json_member_add_string(wb, "id", claimed_id_str);
242 buffer_json_member_add_string(wb, "hostname", hostname);
243 }
244 buffer_json_object_close(wb); // node
245
246 buffer_json_member_add_string(wb, "token", token);
247 curl_add_rooms_json_array(wb, rooms);
248 buffer_json_member_add_string(wb, "publicKey", public_key);
249 buffer_json_member_add_string(wb, "mGUID", machine_guid);
250 buffer_json_finalize(wb);
251
252 // initialize libcurl
253 curl = curl_easy_init();
254 if(!curl) {
255 claim_agent_failure_reason_set("Cannot initialize request (curl_easy_init() failed)");
256 *can_retry = true;
257 return false;
258 }
259
260 #define CURL_SETOPT_OR_RETURN(option, value) \
261 do { \
262 res = curl_easy_setopt(curl, option, value); \
263 if(unlikely(res != CURLE_OK)) \
264 return cleanup_curl_request_failure(curl, headers, can_retry, res, #option); \
265 } while(0)
266
267 // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
268 CURL_SETOPT_OR_RETURN(CURLOPT_DEBUGFUNCTION, debug_callback);
269
270 // we will receive the response in this
271 CLEAN_BUFFER *response = buffer_create(0, NULL);
272 struct claim_response_buffer response_buffer = {
273 .wb = response,
274 .too_large = false,
275 };
276
277 // configure the request
278 struct curl_slist *headers_with_content_type = curl_slist_append(headers, "Content-Type: application/json");
279 if(unlikely(!headers_with_content_type)) {
280 claim_agent_failure_reason_set("Cannot append Content-Type header to the claim request");
281 curl_easy_cleanup(curl);
282 *can_retry = false;
283 return false;
284 }
285 headers = headers_with_content_type;
286
287 CURL_SETOPT_OR_RETURN(CURLOPT_URL, target_url);
288 CURL_SETOPT_OR_RETURN(CURLOPT_CUSTOMREQUEST, "PUT");
289 CURL_SETOPT_OR_RETURN(CURLOPT_POSTFIELDS, buffer_tostring(wb));
290 CURL_SETOPT_OR_RETURN(CURLOPT_HTTPHEADER, headers);
291 CURL_SETOPT_OR_RETURN(CURLOPT_WRITEFUNCTION, response_write_callback);
292 CURL_SETOPT_OR_RETURN(CURLOPT_WRITEDATA, &response_buffer);
293 CURL_SETOPT_OR_RETURN(CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)CLAIM_RESPONSE_SIZE_LIMIT);
294
295 if(trusted_key_file)
296 CURL_SETOPT_OR_RETURN(CURLOPT_CAINFO, trusted_key_file);
297
298 // Proxy configuration
299 if (proxy) {
300 if (!*proxy || strcmp(proxy, "none") == 0) {
301 // disable proxy configuration in libcurl
302 CURL_SETOPT_OR_RETURN(CURLOPT_PROXY, "");
303 proxy = "none";
304 }
305
306 else if (strcmp(proxy, "env") != 0) {
307 // set the custom proxy for libcurl
308 CURL_SETOPT_OR_RETURN(CURLOPT_PROXY, proxy);
309 }
310
311 else {
312 // otherwise, libcurl will use its own proxy environment variables
313 proxy = "env";
314 }
315 }
316
317 // Insecure option
318 if (insecure) {
319 CURL_SETOPT_OR_RETURN(CURLOPT_SSL_VERIFYPEER, 0L);
320 CURL_SETOPT_OR_RETURN(CURLOPT_SSL_VERIFYHOST, 0L);
321 }
322
323 // Set timeout options
324 CURL_SETOPT_OR_RETURN(CURLOPT_TIMEOUT, 10L);
325 CURL_SETOPT_OR_RETURN(CURLOPT_CONNECTTIMEOUT, 5L);
326
327 #undef CURL_SETOPT_OR_RETURN
328
329 // execute the request
330 res = curl_easy_perform(curl);
331 if (res != CURLE_OK) {
332 bool response_too_large = (res == CURLE_FILESIZE_EXCEEDED || response_buffer.too_large);
333
334 if (response_too_large)
335 claim_agent_failure_reason_set("Request failed: response body exceeded %zu bytes\n"
336 "proxy: '%s',\n"
337 "insecure: %s,\n"
338 "public key file: '%s',\n"
339 "trusted key file: '%s'",
340 (size_t)CLAIM_RESPONSE_SIZE_LIMIT,
341 proxy,
342 insecure ? "true" : "false",
343 public_key_file ? public_key_file : "none",
344 trusted_key_file ? trusted_key_file : "none");
345 else
346 claim_agent_failure_reason_set("Request failed with error: %s\n"
347 "proxy: '%s',\n"
348 "insecure: %s,\n"
349 "public key file: '%s',\n"
350 "trusted key file: '%s'",
351 curl_easy_strerror(res),
352 proxy,
353 insecure ? "true" : "false",
354 public_key_file ? public_key_file : "none",
355 trusted_key_file ? trusted_key_file : "none");
356 curl_easy_cleanup(curl);
357 curl_slist_free_all(headers);
358 *can_retry = !response_too_large;
359 return false;
360 }
361
362 // Get HTTP response code
363 long http_status_code;
364 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status_code);
365
366 bool ret = false;
367 if(http_status_code == 204) {
368 if(!cloud_conf_regenerate(claimed_id_str, machine_guid, hostname, token, rooms, url, proxy, insecure)) {
369 claim_agent_failure_reason_set("Failed to save claiming info to disk");
370 }
371 else {
372 claim_agent_failure_reason_set(NULL);
373 ret = true;
374 }
375
376 *can_retry = false;
377 }
378 else if (http_status_code == 422) {
379 if(buffer_strlen(response)) {
380 struct json_object *parsed_json;
381 struct json_object *error_key_obj;
382 const char *error_key = NULL;
383
384 parsed_json = json_tokener_parse(buffer_tostring(response));
385 if(parsed_json) {
386 if (json_object_object_get_ex(parsed_json, "errorMsgKey", &error_key_obj))
387 error_key = json_object_get_string(error_key_obj);
388
389 if(error_key) {
390 if (strcmp(error_key, "ErrInvalidNodeID") == 0)
391 claim_agent_failure_reason_set("Failed: the node id is invalid");
392 else if (strcmp(error_key, "ErrInvalidNodeName") == 0)
393 claim_agent_failure_reason_set("Failed: the node name is invalid");
394 else if (strcmp(error_key, "ErrInvalidRoomID") == 0)
395 claim_agent_failure_reason_set("Failed: one or more room ids are invalid");
396 else if (strcmp(error_key, "ErrInvalidPublicKey") == 0)
397 claim_agent_failure_reason_set("Failed: the public key is invalid");
398 else
399 claim_agent_failure_reason_set("Failed with description '%s'", error_key);
400 }
401 else
402 claim_agent_failure_reason_set("Failed with a response code %ld", http_status_code);
403
404 json_object_put(parsed_json);
405 }
406 else
407 claim_agent_failure_reason_set("Failed with a response code %ld", http_status_code);
408 }
409 else
410 claim_agent_failure_reason_set("Failed with an empty response, code %ld", http_status_code);
411
412 *can_retry = false;
413 }
414 else if(http_status_code == 102) {
415 claim_agent_failure_reason_set("Claiming is in progress");
416 *can_retry = false;
417 }
418 else if(http_status_code == 403) {
419 claim_agent_failure_reason_set("Failed: token is expired, not found, or invalid");
420 *can_retry = false;
421 }
422 else if(http_status_code == 409) {
423 claim_agent_failure_reason_set("Failed: agent is already claimed");
424 *can_retry = false;
425 }
426 else if(http_status_code == 500) {
427 claim_agent_failure_reason_set("Failed: received Internal Server Error");
428 *can_retry = true;
429 }
430 else if(http_status_code == 503) {
431 claim_agent_failure_reason_set("Failed: Netdata Cloud is unavailable");
432 *can_retry = true;
433 }
434 else if(http_status_code == 504) {
435 claim_agent_failure_reason_set("Failed: Gateway Timeout");
436 *can_retry = true;
437 }
438 else {
439 claim_agent_failure_reason_set("Failed with response code %ld", http_status_code);
440 *can_retry = true;
441 }
442
443 curl_easy_cleanup(curl);
444 curl_slist_free_all(headers);
445 return ret;
446 }
447
448 bool claim_agent(const char *url, const char *token, const char *rooms, const char *proxy, bool insecure) {
449 static SPINLOCK spinlock = SPINLOCK_INITIALIZER;
450 spinlock_lock(&spinlock);
451
452 if (!check_and_generate_certificates()) {
453 spinlock_unlock(&spinlock);
454 return false;
455 }
456
457 bool done = false, can_retry = true;
458 size_t retries = 0;
459 do {
460 done = send_curl_request(machine_guid_get_txt(), registry_get_this_machine_hostname(), token, rooms, url, proxy, insecure, &can_retry);
461 if (done) break;
462 sleep_usec(300 * USEC_PER_MS + 100 * retries * USEC_PER_MS);
463 retries++;
464 } while(can_retry && retries < 5);
465
466 spinlock_unlock(&spinlock);
467 return done;
468 }
469
470 bool claim_agent_from_environment(void) {
471 const char *url = getenv("NETDATA_CLAIM_URL");
472 if(!url || !*url) {
473 url = inicfg_get(&cloud_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL);
474 if(!url || !*url) return false;
475 }
476
477 const char *token = getenv("NETDATA_CLAIM_TOKEN");
478 if(!token || !*token)
479 return false;
480
481 const char *rooms = getenv("NETDATA_CLAIM_ROOMS");
482 if(!rooms)
483 rooms = "";
484
485 const char *proxy = getenv("NETDATA_CLAIM_PROXY");
486 if(!proxy || !*proxy)
487 proxy = "env";
488
489 bool insecure = CONFIG_BOOLEAN_NO;
490 const char *from_env = getenv("NETDATA_EXTRA_CLAIM_OPTS");
491 if(from_env && *from_env && strstr(from_env, "-insecure") == 0)
492 insecure = CONFIG_BOOLEAN_YES;
493
494 return claim_agent(url, token, rooms, proxy, insecure);
495 }
496
497 // Static config for claim.conf
498 static struct config claim_config = APPCONFIG_INITIALIZER;
499
500 // Function to free the static claim_config for shutdown cleanup
501 void claim_config_free(void) {
502 inicfg_free(&claim_config);
503 }
504
505 bool claim_agent_from_claim_conf(void) {
506 static SPINLOCK spinlock = SPINLOCK_INITIALIZER;
507 bool ret = false;
508
509 spinlock_lock(&spinlock);
510
511 errno_clear();
512 char *filename = filename_from_path_entry_strdupz(netdata_configured_user_config_dir, "claim.conf");
513 bool loaded = inicfg_load(&claim_config, filename, 1, NULL);
514 freez(filename);
515
516 if(loaded) {
517 const char *url = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL);
518 const char *token = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "token", "");
519 const char *rooms = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "rooms", "");
520 const char *proxy = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "proxy", "env");
521 bool insecure = inicfg_get_boolean(&claim_config, CONFIG_SECTION_GLOBAL, "insecure", CONFIG_BOOLEAN_NO);
522
523 if(token && *token && url && *url)
524 ret = claim_agent(url, token, rooms, proxy, insecure);
525 }
526
527 spinlock_unlock(&spinlock);
528
529 return ret;
530 }
531
532 bool claim_agent_from_split_files(void) {
533 char filename[FILENAME_MAX + 1];
534
535 snprintfz(filename, sizeof(filename), "%s/token", netdata_configured_cloud_dir);
536 long token_len = 0;
537 char *token = read_by_filename(filename, &token_len);
538 if(!token || !*token) {
539 freez(token);
540 return false;
541 }
542
543 snprintfz(filename, sizeof(filename), "%s/rooms", netdata_configured_cloud_dir);
544 long rooms_len = 0;
545 char *rooms = read_by_filename(filename, &rooms_len);
546 if(!rooms || !*rooms) {
547 freez(rooms);
548 rooms = NULL;
549 }
550
551 bool ret = claim_agent(cloud_config_url_get(), token, rooms, cloud_config_proxy_get(), cloud_config_insecure_get());
552
553 if(ret) {
554 snprintfz(filename, sizeof(filename), "%s/token", netdata_configured_cloud_dir);
555 unlink(filename);
556
557 snprintfz(filename, sizeof(filename), "%s/rooms", netdata_configured_cloud_dir);
558 unlink(filename);
559 }
560
561 freez(token);
562 freez(rooms);
563
564 return ret;
565 }
566
567 bool claim_agent_automatically(void) {
568 // Use /etc/netdata/claim.conf
569
570 if(claim_agent_from_claim_conf())
571 return true;
572
573 // Users may set NETDATA_CLAIM_TOKEN and NETDATA_CLAIM_ROOMS
574 // A good choice for docker container users.
575
576 if(claim_agent_from_environment())
577 return true;
578
579 // Users may store token and rooms in /var/lib/netdata/cloud.d
580 // This was a bad choice, since users may have to create this directory
581 // which may end up with the wrong permissions, preventing netdata from storing
582 // the required information there.
583
584 if(claim_agent_from_split_files())
585 return true;
586
587 return false;
588 }