diff options
Diffstat (limited to 'src/claim/claim-with-api.c')
-rw-r--r-- | src/claim/claim-with-api.c | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/src/claim/claim-with-api.c b/src/claim/claim-with-api.c new file mode 100644 index 000000000..534d4511a --- /dev/null +++ b/src/claim/claim-with-api.c @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "claim.h" + +#include "registry/registry.h" + +#include <curl/curl.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/err.h> + +static bool check_and_generate_certificates() { + FILE *fp; + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = NULL; + + CLEAN_CHAR_P *private_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "private.pem"); + CLEAN_CHAR_P *public_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "public.pem"); + + // Check if private key exists + fp = fopen(public_key_file, "r"); + if (fp) { + fclose(fp); + return true; + } + + // Generate the RSA key + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (!pctx) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_CTX_new_id() failed"); + return false; + } + + if (EVP_PKEY_keygen_init(pctx) <= 0) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_keygen_init() failed"); + EVP_PKEY_CTX_free(pctx); + return false; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_CTX_set_rsa_keygen_bits() failed"); + EVP_PKEY_CTX_free(pctx); + return false; + } + + if (EVP_PKEY_keygen(pctx, &pkey) <= 0) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_keygen() failed"); + EVP_PKEY_CTX_free(pctx); + return false; + } + + EVP_PKEY_CTX_free(pctx); + + // Save private key + fp = fopen(private_key_file, "wb"); + if (!fp || !PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL)) { + claim_agent_failure_reason_set("Cannot write private key file: %s", private_key_file); + if (fp) fclose(fp); + EVP_PKEY_free(pkey); + return false; + } + fclose(fp); + + // Save public key + fp = fopen(public_key_file, "wb"); + if (!fp || !PEM_write_PUBKEY(fp, pkey)) { + claim_agent_failure_reason_set("Cannot write public key file: %s", public_key_file); + if (fp) fclose(fp); + EVP_PKEY_free(pkey); + return false; + } + fclose(fp); + + EVP_PKEY_free(pkey); + return true; +} + +static size_t response_write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { + BUFFER *wb = stream; + size_t real_size = size * nmemb; + + buffer_memcat(wb, ptr, real_size); + + return real_size; +} + +static const char *curl_add_json_room(BUFFER *wb, const char *start, const char *end) { + size_t len = end - start; + + // copy the item to an new buffer and terminate it + char buf[len + 1]; + memcpy(buf, start, len); + buf[len] = '\0'; + + // add it to the json array + const char *trimmed = trim(buf); // remove leading and trailing spaces + if(trimmed) + buffer_json_add_array_item_string(wb, trimmed); + + // prepare for the next item + start = end + 1; + + // skip multiple separators or spaces + while(*start == ',' || *start == ' ') start++; + + return start; +} + +void curl_add_rooms_json_array(BUFFER *wb, const char *rooms) { + buffer_json_member_add_array(wb, "rooms"); + if(rooms && *rooms) { + const char *start = rooms, *end = NULL; + + // Skip initial separators or spaces + while (*start == ',' || *start == ' ') + start++; + + // Process each item in the comma-separated list + while ((end = strchr(start, ',')) != NULL) + start = curl_add_json_room(wb, start, end); + + // Process the last item if any + if (*start) + curl_add_json_room(wb, start, &start[strlen(start)]); + } + buffer_json_array_close(wb); +} + +static int debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr) { + (void)handle; // Unused + (void)userptr; // Unused + + if (type == CURLINFO_TEXT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Info: %s", data); + else if (type == CURLINFO_HEADER_OUT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send header: %.*s", (int)size, data); + else if (type == CURLINFO_DATA_OUT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send data: %.*s", (int)size, data); + else if (type == CURLINFO_SSL_DATA_OUT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send SSL data: %.*s", (int)size, data); + else if (type == CURLINFO_HEADER_IN) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive header: %.*s", (int)size, data); + else if (type == CURLINFO_DATA_IN) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive data: %.*s", (int)size, data); + else if (type == CURLINFO_SSL_DATA_IN) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive SSL data: %.*s", (int)size, data); + + return 0; +} + +static bool send_curl_request(const char *machine_guid, const char *hostname, const char *token, const char *rooms, const char *url, const char *proxy, int insecure, bool *can_retry) { + CURL *curl; + CURLcode res; + char target_url[2048]; + char public_key[2048] = ""; + FILE *fp; + struct curl_slist *headers = NULL; + + // create a new random claim id + nd_uuid_t claimed_id; + uuid_generate_random(claimed_id); + char claimed_id_str[UUID_STR_LEN]; + uuid_unparse_lower(claimed_id, claimed_id_str); + + // generate the URL to post + snprintf(target_url, sizeof(target_url), "%s%sapi/v1/spaces/nodes/%s", + url, strendswith(url, "/") ? "" : "/", claimed_id_str); + + // Read the public key + CLEAN_CHAR_P *public_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "public.pem"); + fp = fopen(public_key_file, "r"); + if (!fp || fread(public_key, 1, sizeof(public_key), fp) == 0) { + claim_agent_failure_reason_set("cannot read public key file '%s'", public_key_file); + if (fp) fclose(fp); + *can_retry = false; + return false; + } + fclose(fp); + + // check if we have trusted.pem + // or cloud_fullchain.pem, for backwards compatibility + CLEAN_CHAR_P *trusted_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "trusted.pem"); + fp = fopen(trusted_key_file, "r"); + if(fp) + fclose(fp); + else { + freez(trusted_key_file); + trusted_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "cloud_fullchain.pem"); + fp = fopen(trusted_key_file, "r"); + if(fp) + fclose(fp); + else { + freez(trusted_key_file); + trusted_key_file = NULL; + } + } + + // generate the JSON request message + CLEAN_BUFFER *wb = buffer_create(0, NULL); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + + buffer_json_member_add_object(wb, "node"); + { + buffer_json_member_add_string(wb, "id", claimed_id_str); + buffer_json_member_add_string(wb, "hostname", hostname); + } + buffer_json_object_close(wb); // node + + buffer_json_member_add_string(wb, "token", token); + curl_add_rooms_json_array(wb, rooms); + buffer_json_member_add_string(wb, "publicKey", public_key); + buffer_json_member_add_string(wb, "mGUID", machine_guid); + buffer_json_finalize(wb); + + // initialize libcurl + curl = curl_easy_init(); + if(!curl) { + claim_agent_failure_reason_set("Cannot initialize request (curl_easy_init() failed)"); + *can_retry = true; + return false; + } + + // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback); + + // we will receive the response in this + CLEAN_BUFFER *response = buffer_create(0, NULL); + + // configure the request + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_URL, target_url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buffer_tostring(wb)); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, response_write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); + + if(trusted_key_file) + curl_easy_setopt(curl, CURLOPT_CAINFO, trusted_key_file); + + // Proxy configuration + if (proxy) { + if (!*proxy || strcmp(proxy, "none") == 0) + // disable proxy configuration in libcurl + curl_easy_setopt(curl, CURLOPT_PROXY, ""); + + else if (strcmp(proxy, "env") != 0) + // set the custom proxy for libcurl + curl_easy_setopt(curl, CURLOPT_PROXY, proxy); + + // otherwise, libcurl will use its own proxy environment variables + } + + // Insecure option + if (insecure) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + } + + // Set timeout options + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5); + + // execute the request + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + claim_agent_failure_reason_set("Request failed with error: %s", curl_easy_strerror(res)); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + *can_retry = true; + return false; + } + + // Get HTTP response code + long http_status_code; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status_code); + + bool ret = false; + if(http_status_code == 204) { + if(!cloud_conf_regenerate(claimed_id_str, machine_guid, hostname, token, rooms, url, proxy, insecure)) { + claim_agent_failure_reason_set("Failed to save claiming info to disk"); + } + else { + claim_agent_failure_reason_set(NULL); + ret = true; + } + + *can_retry = false; + } + else if (http_status_code == 422) { + if(buffer_strlen(response)) { + struct json_object *parsed_json; + struct json_object *error_key_obj; + const char *error_key = NULL; + + parsed_json = json_tokener_parse(buffer_tostring(response)); + if(parsed_json) { + if (json_object_object_get_ex(parsed_json, "errorMsgKey", &error_key_obj)) + error_key = json_object_get_string(error_key_obj); + + if (strcmp(error_key, "ErrInvalidNodeID") == 0) + claim_agent_failure_reason_set("Failed: the node id is invalid"); + else if (strcmp(error_key, "ErrInvalidNodeName") == 0) + claim_agent_failure_reason_set("Failed: the node name is invalid"); + else if (strcmp(error_key, "ErrInvalidRoomID") == 0) + claim_agent_failure_reason_set("Failed: one or more room ids are invalid"); + else if (strcmp(error_key, "ErrInvalidPublicKey") == 0) + claim_agent_failure_reason_set("Failed: the public key is invalid"); + else + claim_agent_failure_reason_set("Failed with description '%s'", error_key); + + json_object_put(parsed_json); + } + else + claim_agent_failure_reason_set("Failed with a response code %ld", http_status_code); + } + else + claim_agent_failure_reason_set("Failed with an empty response, code %ld", http_status_code); + + *can_retry = false; + } + else if(http_status_code == 102) { + claim_agent_failure_reason_set("Claiming is in progress"); + *can_retry = false; + } + else if(http_status_code == 403) { + claim_agent_failure_reason_set("Failed: token is expired, not found, or invalid"); + *can_retry = false; + } + else if(http_status_code == 409) { + claim_agent_failure_reason_set("Failed: agent is already claimed"); + *can_retry = false; + } + else if(http_status_code == 500) { + claim_agent_failure_reason_set("Failed: received Internal Server Error"); + *can_retry = true; + } + else if(http_status_code == 503) { + claim_agent_failure_reason_set("Failed: Netdata Cloud is unavailable"); + *can_retry = true; + } + else if(http_status_code == 504) { + claim_agent_failure_reason_set("Failed: Gateway Timeout"); + *can_retry = true; + } + else { + claim_agent_failure_reason_set("Failed with response code %ld", http_status_code); + *can_retry = true; + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + return ret; +} + +bool claim_agent(const char *url, const char *token, const char *rooms, const char *proxy, bool insecure) { + static SPINLOCK spinlock = NETDATA_SPINLOCK_INITIALIZER; + spinlock_lock(&spinlock); + + if (!check_and_generate_certificates()) { + spinlock_unlock(&spinlock); + return false; + } + + bool done = false, can_retry = true; + size_t retries = 0; + do { + done = send_curl_request(registry_get_this_machine_guid(), registry_get_this_machine_hostname(), token, rooms, url, proxy, insecure, &can_retry); + if (done) break; + sleep_usec(300 * USEC_PER_MS + 100 * retries * USEC_PER_MS); + retries++; + } while(can_retry && retries < 5); + + spinlock_unlock(&spinlock); + return done; +} + +bool claim_agent_from_environment(void) { + const char *url = getenv("NETDATA_CLAIM_URL"); + if(!url || !*url) { + url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL); + if(!url || !*url) return false; + } + + const char *token = getenv("NETDATA_CLAIM_TOKEN"); + if(!token || !*token) + return false; + + const char *rooms = getenv("NETDATA_CLAIM_ROOMS"); + if(!rooms) + rooms = ""; + + const char *proxy = getenv("NETDATA_CLAIM_PROXY"); + if(!proxy || !*proxy) + proxy = ""; + + bool insecure = CONFIG_BOOLEAN_NO; + const char *from_env = getenv("NETDATA_EXTRA_CLAIM_OPTS"); + if(from_env && *from_env && strstr(from_env, "-insecure") == 0) + insecure = CONFIG_BOOLEAN_YES; + + return claim_agent(url, token, rooms, proxy, insecure); +} + +bool claim_agent_from_claim_conf(void) { + static struct config claim_config = APPCONFIG_INITIALIZER; + static SPINLOCK spinlock = NETDATA_SPINLOCK_INITIALIZER; + bool ret = false; + + spinlock_lock(&spinlock); + + errno_clear(); + char *filename = filename_from_path_entry_strdupz(netdata_configured_user_config_dir, "claim.conf"); + bool loaded = appconfig_load(&claim_config, filename, 1, NULL); + freez(filename); + + if(loaded) { + const char *url = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL); + const char *token = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "token", ""); + const char *rooms = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "rooms", ""); + const char *proxy = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "proxy", ""); + bool insecure = appconfig_get_boolean(&claim_config, CONFIG_SECTION_GLOBAL, "insecure", CONFIG_BOOLEAN_NO); + + if(token && *token && url && *url) + ret = claim_agent(url, token, rooms, proxy, insecure); + } + + spinlock_unlock(&spinlock); + + return ret; +} + +bool claim_agent_from_split_files(void) { + char filename[FILENAME_MAX + 1]; + + snprintfz(filename, sizeof(filename), "%s/token", netdata_configured_cloud_dir); + long token_len = 0; + char *token = read_by_filename(filename, &token_len); + if(!token || !*token) { + freez(token); + return false; + } + + snprintfz(filename, sizeof(filename), "%s/rooms", netdata_configured_cloud_dir); + long rooms_len = 0; + char *rooms = read_by_filename(filename, &rooms_len); + if(!rooms || !*rooms) { + freez(rooms); + rooms = NULL; + } + + bool ret = claim_agent(cloud_config_url_get(), token, rooms, cloud_config_proxy_get(), cloud_config_insecure_get()); + + if(ret) { + snprintfz(filename, sizeof(filename), "%s/token", netdata_configured_cloud_dir); + unlink(filename); + + snprintfz(filename, sizeof(filename), "%s/rooms", netdata_configured_cloud_dir); + unlink(filename); + } + + return ret; +} + +bool claim_agent_automatically(void) { + // Use /etc/netdata/claim.conf + + if(claim_agent_from_claim_conf()) + return true; + + // Users may set NETDATA_CLAIM_TOKEN and NETDATA_CLAIM_ROOMS + // A good choice for docker container users. + + if(claim_agent_from_environment()) + return true; + + // Users may store token and rooms in /var/lib/netdata/cloud.d + // This was a bad choice, since users may have to create this directory + // which may end up with the wrong permissions, preventing netdata from storing + // the required information there. + + if(claim_agent_from_split_files()) + return true; + + return false; +} |