master
c 285 lines 9.16 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 /*
4 * /api/v3/settings
5 *
6 * QUERY STRING PARAMETERS:
7 * - file=a file name (alphanumerics, dashes, underscores)
8 * When the user is not authenticated with a bearer token
9 * only the 'default' file is allowed.
10 * Authenticated users can create, store and update any
11 * settings file.
12 *
13 * HTTP METHODS
14 * - GET to retrieve a file
15 * - PUT to create or update a file
16 *
17 * PAYLOAD
18 * - The payload MUST have the member 'version'.
19 * - The payload MAY have anything else.
20 * - The maximum payload size in JSON is 20MiB.
21 * - When updating the payload, the caller must specify the
22 * version of the existing file. If this check fails,
23 * Netdata will return 409 (conflict).
24 * When the caller receives 409, it means there are updates
25 * in the payload outside its control and the object MUST
26 * be loaded again to find its current version to update it.
27 * After loading it, the caller must reapply the changes and
28 * PUT it again.
29 * - Netdata will increase the version on every PUT action.
30 * So, the payload MUST specify the version found on disk
31 * but, Netdata will increment the version before saving it.
32 */
33
34 #include "api_v3_calls.h"
35
36 #define MAX_SETTINGS_SIZE_BYTES (20 * 1024 * 1024)
37
38 // we need an r/w spinlock to ensure that reads and write do not happen
39 // concurrently for settings files
40 static RW_SPINLOCK settings_spinlock = RW_SPINLOCK_INITIALIZER;
41
42 static inline void settings_path(char out[FILENAME_MAX]) {
43 filename_from_path_entry(out, netdata_configured_varlib_dir, "settings", NULL);
44 }
45
46 static inline void settings_filename(char out[FILENAME_MAX], const char *file, const char *extension) {
47 char path[FILENAME_MAX];
48 settings_path(path);
49 filename_from_path_entry(out, path, file, extension);
50 }
51
52 static inline bool settings_ensure_path_exists(void) {
53 char path[FILENAME_MAX];
54 settings_path(path);
55 return filename_is_dir(path, true);
56 }
57
58 static inline size_t settings_extract_json_version(const char *json) {
59 if(!json || !*json) return 0;
60
61 // Parse the JSON string into a JSON-C object
62 CLEAN_JSON_OBJECT *jobj = json_tokener_parse(json);
63 if (jobj == NULL)
64 return 0;
65
66 // Access the "version" field
67 struct json_object *version_obj;
68 if (json_object_object_get_ex(jobj, "version", &version_obj))
69 // Extract the integer value of the version
70 return (size_t)json_object_get_int(version_obj);
71
72 return 0;
73 }
74
75 static inline void settings_initial_version(BUFFER *wb) {
76 buffer_reset(wb);
77 buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
78 buffer_json_member_add_uint64(wb, "version", 1);
79 buffer_json_finalize(wb);
80 }
81
82 static inline void settings_get(BUFFER *wb, const char *file, bool have_lock) {
83 char filename[FILENAME_MAX];
84 settings_filename(filename, file, NULL);
85
86 buffer_reset(wb);
87
88 if(!have_lock)
89 rw_spinlock_read_lock(&settings_spinlock);
90
91 bool rc = read_txt_file_to_buffer(filename, wb, MAX_SETTINGS_SIZE_BYTES);
92
93 if(!have_lock)
94 rw_spinlock_read_unlock(&settings_spinlock);
95
96 if(rc) {
97 size_t version = settings_extract_json_version(buffer_tostring(wb));
98 if (!version) {
99 nd_log(NDLS_DAEMON, NDLP_ERR, "file '%s' cannot be parsed to extract version", filename);
100 settings_initial_version(wb);
101 }
102 else {
103 wb->content_type = CT_APPLICATION_JSON;
104 buffer_no_cacheable(wb);
105 }
106 }
107 else
108 settings_initial_version(wb);
109 }
110
111 static inline size_t settings_get_version(const char *path, bool have_lock) {
112 CLEAN_BUFFER *wb = buffer_create(0, NULL);
113 settings_get(wb, path, have_lock);
114
115 return settings_extract_json_version(buffer_tostring(wb));
116 }
117
118 static inline int settings_put(struct web_client *w, char *file) {
119 rw_spinlock_write_lock(&settings_spinlock);
120
121 if(!settings_ensure_path_exists()) {
122 rw_spinlock_write_unlock(&settings_spinlock);
123 return rrd_call_function_error(
124 w->response.data,
125 "Settings path cannot be created or accessed.",
126 HTTP_RESP_BAD_REQUEST);
127 }
128
129 size_t old_version = settings_get_version(file, true);
130
131 // Parse the JSON string into a JSON-C object
132 CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(w->payload));
133 if (jobj == NULL) {
134 rw_spinlock_write_unlock(&settings_spinlock);
135 return rrd_call_function_error(
136 w->response.data,
137 "Payload cannot be parsed as a JSON object",
138 HTTP_RESP_BAD_REQUEST);
139 }
140
141 // Access the "version" field
142 struct json_object *version_obj;
143 if (!json_object_object_get_ex(jobj, "version", &version_obj)) {
144 rw_spinlock_write_unlock(&settings_spinlock);
145 return rrd_call_function_error(
146 w->response.data,
147 "Field version is not found in payload",
148 HTTP_RESP_BAD_REQUEST);
149 }
150
151 size_t new_version = (size_t)json_object_get_int(version_obj);
152
153 if (old_version != new_version) {
154 rw_spinlock_write_unlock(&settings_spinlock);
155 return rrd_call_function_error(
156 w->response.data,
157 "Payload version does not match the version of the stored object",
158 HTTP_RESP_CONFLICT);
159 }
160
161 new_version++;
162 // Set the new version back into the JSON object
163 json_object_object_add(jobj, "version", json_object_new_int((int)new_version));
164
165 // Convert the updated JSON object back to a string
166 const char *updated_json_str = json_object_to_json_string(jobj);
167
168 char tmp_filename[FILENAME_MAX];
169 settings_filename(tmp_filename, file, "new");
170
171 // Save the updated JSON string to a file
172 FILE *fp = fopen(tmp_filename, "w");
173 if (fp == NULL) {
174 rw_spinlock_write_unlock(&settings_spinlock);
175 nd_log(NDLS_DAEMON, NDLP_ERR, "cannot open/create settings file '%s'", tmp_filename);
176 return rrd_call_function_error(
177 w->response.data,
178 "Cannot create payload file '%s'",
179 HTTP_RESP_INTERNAL_SERVER_ERROR);
180 }
181 size_t len = strlen(updated_json_str);
182 if(fwrite(updated_json_str, 1, len, fp) != len) {
183 fclose(fp);
184 unlink(tmp_filename);
185 rw_spinlock_write_unlock(&settings_spinlock);
186 nd_log(NDLS_DAEMON, NDLP_ERR, "cannot save settings to file '%s'", tmp_filename);
187 return rrd_call_function_error(
188 w->response.data,
189 "Cannot save payload to file '%s'",
190 HTTP_RESP_INTERNAL_SERVER_ERROR);
191 }
192 fclose(fp);
193
194 char filename[FILENAME_MAX];
195 settings_filename(filename, file, NULL);
196
197 bool renamed = rename(tmp_filename, filename) == 0;
198
199 rw_spinlock_write_unlock(&settings_spinlock);
200
201 if(!renamed) {
202 nd_log(NDLS_DAEMON, NDLP_ERR, "cannot rename file '%s' to '%s'", tmp_filename, filename);
203 return rrd_call_function_error(
204 w->response.data,
205 "Failed to move the payload file to its final location",
206 HTTP_RESP_INTERNAL_SERVER_ERROR);
207 }
208
209 return rrd_call_function_error(
210 w->response.data,
211 "OK",
212 HTTP_RESP_OK);
213 }
214
215 static inline bool is_settings_file_valid(char *file) {
216 char *s = file;
217
218 if(!s || !*s)
219 return false;
220
221 while(*s) {
222 if(!isalnum((uint8_t)*s) && *s != '-' && *s != '_')
223 return false;
224 s++;
225 }
226
227 return true;
228 }
229
230 int api_v3_settings(RRDHOST *host, struct web_client *w, char *url) {
231 char *file = NULL;
232
233 while(url) {
234 char *value = strsep_skip_consecutive_separators(&url, "&");
235 if(!value || !*value) continue;
236
237 char *name = strsep_skip_consecutive_separators(&value, "=");
238 if(!name || !*name) continue;
239 if(!value || !*value) continue;
240
241 // name and value are now the parameters
242 // they are not null and not empty
243
244 if(!strcmp(name, "file"))
245 file = value;
246 }
247
248 if(!is_settings_file_valid(file))
249 return rrd_call_function_error(
250 w->response.data,
251 "Invalid settings file given.",
252 HTTP_RESP_BAD_REQUEST);
253
254 if(host != localhost)
255 return rrd_call_function_error(
256 w->response.data,
257 "Settings API is only allowed for the agent node.",
258 HTTP_RESP_BAD_REQUEST);
259
260 if(w->user_auth.method != USER_AUTH_METHOD_BEARER && strcmp(file, "default") != 0)
261 return rrd_call_function_error(
262 w->response.data,
263 "Only the 'default' settings file is allowed for anonymous users",
264 HTTP_RESP_BAD_REQUEST);
265
266 switch(w->mode) {
267 case HTTP_REQUEST_MODE_GET:
268 settings_get(w->response.data, file, false);
269 return HTTP_RESP_OK;
270
271 case HTTP_REQUEST_MODE_PUT:
272 if(!w->payload || !buffer_strlen(w->payload))
273 return rrd_call_function_error(
274 w->response.data,
275 "Settings API PUT action requires a payload.",
276 HTTP_RESP_BAD_REQUEST);
277
278 return settings_put(w, file);
279
280 default:
281 return rrd_call_function_error(w->response.data,
282 "Invalid HTTP mode. HTTP modes GET and PUT are supported.",
283 HTTP_RESP_BAD_REQUEST);
284 }
285 }