src/claim/claim-with-api.c
Rama master · réplica de lectura del repositorio original.
// 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>
#define CLAIM_RESPONSE_SIZE_LIMIT (10 * 1024 * 1024)
struct claim_response_buffer {
BUFFER *wb;
bool too_large;
};
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 the public key exists; it is the completion marker for the pair.
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;
}
if (fclose(fp) != 0) {
claim_agent_failure_reason_set("Cannot write private key file: %s", private_key_file);
EVP_PKEY_free(pkey);
return false;
}
// 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);
unlink(public_key_file);
}
EVP_PKEY_free(pkey);
return false;
}
if (fclose(fp) != 0) {
claim_agent_failure_reason_set("Cannot write public key file: %s", public_key_file);
unlink(public_key_file);
EVP_PKEY_free(pkey);
return false;
}
EVP_PKEY_free(pkey);
return true;
}
static size_t response_write_callback(void *ptr, size_t size, size_t nmemb, void *stream) {
struct claim_response_buffer *response = stream;
if (unlikely(nmemb && size > SIZE_MAX / nmemb)) {
response->too_large = true;
return 0;
}
size_t real_size = size * nmemb;
if (unlikely(real_size > CLAIM_RESPONSE_SIZE_LIMIT - buffer_strlen(response->wb))) {
response->too_large = true;
return 0;
}
buffer_memcat(response->wb, ptr, real_size);
return real_size;
}
static const char *curl_add_json_room(BUFFER *wb, const char *start, const char *end, bool last_item)
{
size_t len = end - start;
// copy the item to an new buffer and terminate it
char *buf = mallocz(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);
freez(buf);
if (last_item)
return NULL;
// 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, false);
// Process the last item if any
if (*start)
curl_add_json_room(wb, start, &start[strlen(start)], true);
}
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 cleanup_curl_request_failure(CURL *curl, struct curl_slist *headers, bool *can_retry, CURLcode res,
const char *option) {
claim_agent_failure_reason_set("Cannot configure request (%s failed: %s)", option, curl_easy_strerror(res));
curl_easy_cleanup(curl);
if(headers)
curl_slist_free_all(headers);
*can_retry = false;
return false;
}
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) {
CURL *curl;
CURLcode res;
char target_url[2048];
char public_key[2048] = "";
size_t public_key_bytes_read = 0;
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 || (public_key_bytes_read = fread(public_key, 1, sizeof(public_key) - 1, 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;
}
public_key[public_key_bytes_read] = '\0';
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;
}
#define CURL_SETOPT_OR_RETURN(option, value) \
do { \
res = curl_easy_setopt(curl, option, value); \
if(unlikely(res != CURLE_OK)) \
return cleanup_curl_request_failure(curl, headers, can_retry, res, #option); \
} while(0)
// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
CURL_SETOPT_OR_RETURN(CURLOPT_DEBUGFUNCTION, debug_callback);
// we will receive the response in this
CLEAN_BUFFER *response = buffer_create(0, NULL);
struct claim_response_buffer response_buffer = {
.wb = response,
.too_large = false,
};
// configure the request
struct curl_slist *headers_with_content_type = curl_slist_append(headers, "Content-Type: application/json");
if(unlikely(!headers_with_content_type)) {
claim_agent_failure_reason_set("Cannot append Content-Type header to the claim request");
curl_easy_cleanup(curl);
*can_retry = false;
return false;
}
headers = headers_with_content_type;
CURL_SETOPT_OR_RETURN(CURLOPT_URL, target_url);
CURL_SETOPT_OR_RETURN(CURLOPT_CUSTOMREQUEST, "PUT");
CURL_SETOPT_OR_RETURN(CURLOPT_POSTFIELDS, buffer_tostring(wb));
CURL_SETOPT_OR_RETURN(CURLOPT_HTTPHEADER, headers);
CURL_SETOPT_OR_RETURN(CURLOPT_WRITEFUNCTION, response_write_callback);
CURL_SETOPT_OR_RETURN(CURLOPT_WRITEDATA, &response_buffer);
CURL_SETOPT_OR_RETURN(CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)CLAIM_RESPONSE_SIZE_LIMIT);
if(trusted_key_file)
CURL_SETOPT_OR_RETURN(CURLOPT_CAINFO, trusted_key_file);
// Proxy configuration
if (proxy) {
if (!*proxy || strcmp(proxy, "none") == 0) {
// disable proxy configuration in libcurl
CURL_SETOPT_OR_RETURN(CURLOPT_PROXY, "");
proxy = "none";
}
else if (strcmp(proxy, "env") != 0) {
// set the custom proxy for libcurl
CURL_SETOPT_OR_RETURN(CURLOPT_PROXY, proxy);
}
else {
// otherwise, libcurl will use its own proxy environment variables
proxy = "env";
}
}
// Insecure option
if (insecure) {
CURL_SETOPT_OR_RETURN(CURLOPT_SSL_VERIFYPEER, 0L);
CURL_SETOPT_OR_RETURN(CURLOPT_SSL_VERIFYHOST, 0L);
}
// Set timeout options
CURL_SETOPT_OR_RETURN(CURLOPT_TIMEOUT, 10L);
CURL_SETOPT_OR_RETURN(CURLOPT_CONNECTTIMEOUT, 5L);
#undef CURL_SETOPT_OR_RETURN
// execute the request
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
bool response_too_large = (res == CURLE_FILESIZE_EXCEEDED || response_buffer.too_large);
if (response_too_large)
claim_agent_failure_reason_set("Request failed: response body exceeded %zu bytes\n"
"proxy: '%s',\n"
"insecure: %s,\n"
"public key file: '%s',\n"
"trusted key file: '%s'",
(size_t)CLAIM_RESPONSE_SIZE_LIMIT,
proxy,
insecure ? "true" : "false",
public_key_file ? public_key_file : "none",
trusted_key_file ? trusted_key_file : "none");
else
claim_agent_failure_reason_set("Request failed with error: %s\n"
"proxy: '%s',\n"
"insecure: %s,\n"
"public key file: '%s',\n"
"trusted key file: '%s'",
curl_easy_strerror(res),
proxy,
insecure ? "true" : "false",
public_key_file ? public_key_file : "none",
trusted_key_file ? trusted_key_file : "none");
curl_easy_cleanup(curl);
curl_slist_free_all(headers);
*can_retry = !response_too_large;
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(error_key) {
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);
}
else
claim_agent_failure_reason_set("Failed with a response code %ld", http_status_code);
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 = 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(machine_guid_get_txt(), 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;
}
static bool claim_extra_opts_has_token(const char *opts, const char *token, size_t token_len) {
while(opts && *opts) {
while(isspace((uint8_t)*opts))
opts++;
const char *end = opts;
while(*end && !isspace((uint8_t)*end))
end++;
if((size_t)(end - opts) == token_len && memcmp(opts, token, token_len) == 0)
return true;
opts = end;
}
return false;
}
bool claim_agent_from_environment(void) {
const char *url = getenv("NETDATA_CLAIM_URL");
if(!url || !*url) {
url = inicfg_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 = "env";
bool insecure = CONFIG_BOOLEAN_NO;
const char *from_env = getenv("NETDATA_EXTRA_CLAIM_OPTS");
if(claim_extra_opts_has_token(from_env, "-insecure", sizeof("-insecure") - 1))
insecure = CONFIG_BOOLEAN_YES;
return claim_agent(url, token, rooms, proxy, insecure);
}
// Static config for claim.conf
static struct config claim_config = APPCONFIG_INITIALIZER;
// Function to free the static claim_config for shutdown cleanup
void claim_config_free(void) {
inicfg_free(&claim_config);
}
bool claim_agent_from_claim_conf(void) {
static SPINLOCK spinlock = 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 = inicfg_load(&claim_config, filename, 1, NULL);
freez(filename);
if(loaded) {
const char *url = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL);
const char *token = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "token", "");
const char *rooms = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "rooms", "");
const char *proxy = inicfg_get(&claim_config, CONFIG_SECTION_GLOBAL, "proxy", "env");
bool insecure = inicfg_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);
}
freez(token);
freez(rooms);
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;
}