summaryrefslogtreecommitdiffstats
path: root/src/web/api/v3
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-11-25 17:33:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-11-25 17:34:10 +0000
commit83ba6762cc43d9db581b979bb5e3445669e46cc2 (patch)
tree2e69833b43f791ed253a7a20318b767ebe56cdb8 /src/web/api/v3
parentReleasing debian version 1.47.5-1. (diff)
downloadnetdata-83ba6762cc43d9db581b979bb5e3445669e46cc2.tar.xz
netdata-83ba6762cc43d9db581b979bb5e3445669e46cc2.zip
Merging upstream version 2.0.3+dfsg (Closes: #923993, #1042533, #1045145).
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/web/api/v3')
-rw-r--r--src/web/api/v3/api_v3_calls.h11
-rw-r--r--src/web/api/v3/api_v3_me.c37
-rw-r--r--src/web/api/v3/api_v3_settings.c285
3 files changed, 333 insertions, 0 deletions
diff --git a/src/web/api/v3/api_v3_calls.h b/src/web/api/v3/api_v3_calls.h
new file mode 100644
index 000000000..4cee766fd
--- /dev/null
+++ b/src/web/api/v3/api_v3_calls.h
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NETDATA_API_V3_CALLS_H
+#define NETDATA_API_V3_CALLS_H
+
+#include "../web_api_v3.h"
+
+int api_v3_settings(RRDHOST *host, struct web_client *w, char *url);
+int api_v3_me(RRDHOST *host, struct web_client *w, char *url);
+
+#endif //NETDATA_API_V3_CALLS_H
diff --git a/src/web/api/v3/api_v3_me.c b/src/web/api/v3/api_v3_me.c
new file mode 100644
index 000000000..39ba2c29b
--- /dev/null
+++ b/src/web/api/v3/api_v3_me.c
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "api_v3_calls.h"
+
+int api_v3_me(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) {
+ BUFFER *wb = w->response.data;
+ buffer_reset(wb);
+ buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
+
+ const char *auth;
+ switch(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_CLOUD|WEB_CLIENT_FLAG_AUTH_BEARER|WEB_CLIENT_FLAG_AUTH_GOD)) {
+ case WEB_CLIENT_FLAG_AUTH_CLOUD:
+ auth = "cloud";
+ break;
+
+ case WEB_CLIENT_FLAG_AUTH_BEARER:
+ auth = "bearer";
+ break;
+
+ case WEB_CLIENT_FLAG_AUTH_GOD:
+ auth = "god";
+ break;
+
+ default:
+ auth = "none";
+ break;
+ }
+ buffer_json_member_add_string(wb, "auth", auth);
+
+ buffer_json_member_add_uuid(wb, "cloud_account_id", w->auth.cloud_account_id);
+ buffer_json_member_add_string(wb, "client_name", w->auth.client_name);
+ http_access2buffer_json_array(wb, "access", w->access);
+ buffer_json_member_add_string(wb, "user_role", http_id2user_role(w->user_role));
+
+ buffer_json_finalize(wb);
+ return HTTP_RESP_OK;
+}
diff --git a/src/web/api/v3/api_v3_settings.c b/src/web/api/v3/api_v3_settings.c
new file mode 100644
index 000000000..3b02e6b61
--- /dev/null
+++ b/src/web/api/v3/api_v3_settings.c
@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+/*
+ * /api/v3/settings
+ *
+ * QUERY STRING PARAMETERS:
+ * - file=a file name (alphanumerics, dashes, underscores)
+ * When the user is not authenticated with a bearer token
+ * only the 'default' file is allowed.
+ * Authenticated users can create, store and update any
+ * settings file.
+ *
+ * HTTP METHODS
+ * - GET to retrieve a file
+ * - PUT to create or update a file
+ *
+ * PAYLOAD
+ * - The payload MUST have the member 'version'.
+ * - The payload MAY have anything else.
+ * - The maximum payload size in JSON is 20MiB.
+ * - When updating the payload, the caller must specify the
+ * version of the existing file. If this check fails,
+ * Netdata will return 409 (conflict).
+ * When the caller receives 409, it means there are updates
+ * in the payload outside its control and the object MUST
+ * be loaded again to find its current version to update it.
+ * After loading it, the caller must reapply the changes and
+ * PUT it again.
+ * - Netdata will increase the version on every PUT action.
+ * So, the payload MUST specify the version found on disk
+ * but, Netdata will increment the version before saving it.
+ */
+
+#include "api_v3_calls.h"
+
+#define MAX_SETTINGS_SIZE_BYTES (20 * 1024 * 1024)
+
+// we need an r/w spinlock to ensure that reads and write do not happen
+// concurrently for settings files
+static RW_SPINLOCK settings_spinlock = NETDATA_RW_SPINLOCK_INITIALIZER;
+
+static inline void settings_path(char out[FILENAME_MAX]) {
+ filename_from_path_entry(out, netdata_configured_varlib_dir, "settings", NULL);
+}
+
+static inline void settings_filename(char out[FILENAME_MAX], const char *file, const char *extension) {
+ char path[FILENAME_MAX];
+ settings_path(path);
+ filename_from_path_entry(out, path, file, extension);
+}
+
+static inline bool settings_ensure_path_exists(void) {
+ char path[FILENAME_MAX];
+ settings_path(path);
+ return filename_is_dir(path, true);
+}
+
+static inline size_t settings_extract_json_version(const char *json) {
+ if(!json || !*json) return 0;
+
+ // Parse the JSON string into a JSON-C object
+ CLEAN_JSON_OBJECT *jobj = json_tokener_parse(json);
+ if (jobj == NULL)
+ return 0;
+
+ // Access the "version" field
+ struct json_object *version_obj;
+ if (json_object_object_get_ex(jobj, "version", &version_obj))
+ // Extract the integer value of the version
+ return (size_t)json_object_get_int(version_obj);
+
+ return 0;
+}
+
+static inline void settings_initial_version(BUFFER *wb) {
+ buffer_reset(wb);
+ buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
+ buffer_json_member_add_uint64(wb, "version", 1);
+ buffer_json_finalize(wb);
+}
+
+static inline void settings_get(BUFFER *wb, const char *file, bool have_lock) {
+ char filename[FILENAME_MAX];
+ settings_filename(filename, file, NULL);
+
+ buffer_reset(wb);
+
+ if(!have_lock)
+ rw_spinlock_read_lock(&settings_spinlock);
+
+ bool rc = read_txt_file_to_buffer(filename, wb, MAX_SETTINGS_SIZE_BYTES);
+
+ if(!have_lock)
+ rw_spinlock_read_unlock(&settings_spinlock);
+
+ if(rc) {
+ size_t version = settings_extract_json_version(buffer_tostring(wb));
+ if (!version) {
+ nd_log(NDLS_DAEMON, NDLP_ERR, "file '%s' cannot be parsed to extract version", filename);
+ settings_initial_version(wb);
+ }
+ else {
+ wb->content_type = CT_APPLICATION_JSON;
+ buffer_no_cacheable(wb);
+ }
+ }
+ else
+ settings_initial_version(wb);
+}
+
+static inline size_t settings_get_version(const char *path, bool have_lock) {
+ CLEAN_BUFFER *wb = buffer_create(0, NULL);
+ settings_get(wb, path, have_lock);
+
+ return settings_extract_json_version(buffer_tostring(wb));
+}
+
+static inline int settings_put(struct web_client *w, char *file) {
+ rw_spinlock_write_lock(&settings_spinlock);
+
+ if(!settings_ensure_path_exists()) {
+ rw_spinlock_write_unlock(&settings_spinlock);
+ return rrd_call_function_error(
+ w->response.data,
+ "Settings path cannot be created or accessed.",
+ HTTP_RESP_BAD_REQUEST);
+ }
+
+ size_t old_version = settings_get_version(file, true);
+
+ // Parse the JSON string into a JSON-C object
+ CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(w->payload));
+ if (jobj == NULL) {
+ rw_spinlock_write_unlock(&settings_spinlock);
+ return rrd_call_function_error(
+ w->response.data,
+ "Payload cannot be parsed as a JSON object",
+ HTTP_RESP_BAD_REQUEST);
+ }
+
+ // Access the "version" field
+ struct json_object *version_obj;
+ if (!json_object_object_get_ex(jobj, "version", &version_obj)) {
+ rw_spinlock_write_unlock(&settings_spinlock);
+ return rrd_call_function_error(
+ w->response.data,
+ "Field version is not found in payload",
+ HTTP_RESP_BAD_REQUEST);
+ }
+
+ size_t new_version = (size_t)json_object_get_int(version_obj);
+
+ if (old_version != new_version) {
+ rw_spinlock_write_unlock(&settings_spinlock);
+ return rrd_call_function_error(
+ w->response.data,
+ "Payload version does not match the version of the stored object",
+ HTTP_RESP_CONFLICT);
+ }
+
+ new_version++;
+ // Set the new version back into the JSON object
+ json_object_object_add(jobj, "version", json_object_new_int((int)new_version));
+
+ // Convert the updated JSON object back to a string
+ const char *updated_json_str = json_object_to_json_string(jobj);
+
+ char tmp_filename[FILENAME_MAX];
+ settings_filename(tmp_filename, file, "new");
+
+ // Save the updated JSON string to a file
+ FILE *fp = fopen(tmp_filename, "w");
+ if (fp == NULL) {
+ rw_spinlock_write_unlock(&settings_spinlock);
+ nd_log(NDLS_DAEMON, NDLP_ERR, "cannot open/create settings file '%s'", tmp_filename);
+ return rrd_call_function_error(
+ w->response.data,
+ "Cannot create payload file '%s'",
+ HTTP_RESP_INTERNAL_SERVER_ERROR);
+ }
+ size_t len = strlen(updated_json_str);
+ if(fwrite(updated_json_str, 1, len, fp) != len) {
+ fclose(fp);
+ unlink(tmp_filename);
+ rw_spinlock_write_unlock(&settings_spinlock);
+ nd_log(NDLS_DAEMON, NDLP_ERR, "cannot save settings to file '%s'", tmp_filename);
+ return rrd_call_function_error(
+ w->response.data,
+ "Cannot save payload to file '%s'",
+ HTTP_RESP_INTERNAL_SERVER_ERROR);
+ }
+ fclose(fp);
+
+ char filename[FILENAME_MAX];
+ settings_filename(filename, file, NULL);
+
+ bool renamed = rename(tmp_filename, filename) == 0;
+
+ rw_spinlock_write_unlock(&settings_spinlock);
+
+ if(!renamed) {
+ nd_log(NDLS_DAEMON, NDLP_ERR, "cannot rename file '%s' to '%s'", tmp_filename, filename);
+ return rrd_call_function_error(
+ w->response.data,
+ "Failed to move the payload file to its final location",
+ HTTP_RESP_INTERNAL_SERVER_ERROR);
+ }
+
+ return rrd_call_function_error(
+ w->response.data,
+ "OK",
+ HTTP_RESP_OK);
+}
+
+static inline bool is_settings_file_valid(char *file) {
+ char *s = file;
+
+ if(!s || !*s)
+ return false;
+
+ while(*s) {
+ if(!isalnum((uint8_t)*s) && *s != '-' && *s != '_')
+ return false;
+ s++;
+ }
+
+ return true;
+}
+
+int api_v3_settings(RRDHOST *host, struct web_client *w, char *url) {
+ char *file = NULL;
+
+ while(url) {
+ char *value = strsep_skip_consecutive_separators(&url, "&");
+ if(!value || !*value) continue;
+
+ char *name = strsep_skip_consecutive_separators(&value, "=");
+ if(!name || !*name) continue;
+ if(!value || !*value) continue;
+
+ // name and value are now the parameters
+ // they are not null and not empty
+
+ if(!strcmp(name, "file"))
+ file = value;
+ }
+
+ if(!is_settings_file_valid(file))
+ return rrd_call_function_error(
+ w->response.data,
+ "Invalid settings file given.",
+ HTTP_RESP_BAD_REQUEST);
+
+ if(host != localhost)
+ return rrd_call_function_error(
+ w->response.data,
+ "Settings API is only allowed for the agent node.",
+ HTTP_RESP_BAD_REQUEST);
+
+ if(web_client_flags_check_auth(w) != WEB_CLIENT_FLAG_AUTH_BEARER && strcmp(file, "default") != 0)
+ return rrd_call_function_error(
+ w->response.data,
+ "Only the 'default' settings file is allowed for anonymous users",
+ HTTP_RESP_BAD_REQUEST);
+
+ switch(w->mode) {
+ case HTTP_REQUEST_MODE_GET:
+ settings_get(w->response.data, file, false);
+ return HTTP_RESP_OK;
+
+ case HTTP_REQUEST_MODE_PUT:
+ if(!w->payload || !buffer_strlen(w->payload))
+ return rrd_call_function_error(
+ w->response.data,
+ "Settings API PUT action requires a payload.",
+ HTTP_RESP_BAD_REQUEST);
+
+ return settings_put(w, file);
+
+ default:
+ return rrd_call_function_error(w->response.data,
+ "Invalid HTTP mode. HTTP modes GET and PUT are supported.",
+ HTTP_RESP_BAD_REQUEST);
+ }
+}