Line data Source code
1 : /*
2 : * Copyright 2015-2024 the Pacemaker project contributors
3 : *
4 : * The version control history for this file may have further details.
5 : *
6 : * This source code is licensed under the GNU Lesser General Public License
7 : * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8 : */
9 :
10 : #include <crm_internal.h>
11 :
12 : #include <stdio.h>
13 : #include <unistd.h>
14 : #include <string.h>
15 : #include <stdlib.h>
16 : #include <md5.h>
17 :
18 : #include <crm/crm.h>
19 : #include <crm/common/xml.h>
20 : #include "crmcommon_private.h"
21 :
22 : #define BEST_EFFORT_STATUS 0
23 :
24 : /*!
25 : * \internal
26 : * \brief Dump XML in a format used with v1 digests
27 : *
28 : * \param[in] xml Root of XML to dump
29 : *
30 : * \return Newly allocated buffer containing dumped XML
31 : */
32 : static GString *
33 0 : dump_xml_for_digest(xmlNodePtr xml)
34 : {
35 0 : GString *buffer = g_string_sized_new(1024);
36 :
37 : /* for compatibility with the old result which is used for v1 digests */
38 : g_string_append_c(buffer, ' ');
39 0 : pcmk__xml_string(xml, 0, buffer, 0);
40 : g_string_append_c(buffer, '\n');
41 :
42 0 : return buffer;
43 : }
44 :
45 : /*!
46 : * \brief Calculate and return v1 digest of XML tree
47 : *
48 : * \param[in] input Root of XML to digest
49 : * \param[in] sort Whether to sort the XML before calculating digest
50 : * \param[in] ignored Not used
51 : *
52 : * \return Newly allocated string containing digest
53 : * \note Example return value: "c048eae664dba840e1d2060f00299e9d"
54 : */
55 : static char *
56 0 : calculate_xml_digest_v1(xmlNode *input, gboolean sort, gboolean ignored)
57 : {
58 0 : char *digest = NULL;
59 0 : GString *buffer = NULL;
60 0 : xmlNode *copy = NULL;
61 :
62 0 : if (sort) {
63 0 : crm_trace("Sorting xml...");
64 0 : copy = sorted_xml(input, NULL, TRUE);
65 0 : crm_trace("Done");
66 0 : input = copy;
67 : }
68 :
69 0 : buffer = dump_xml_for_digest(input);
70 0 : CRM_CHECK(buffer->len > 0, free_xml(copy);
71 : g_string_free(buffer, TRUE);
72 : return NULL);
73 :
74 0 : digest = crm_md5sum((const char *) buffer->str);
75 0 : crm_log_xml_trace(input, "digest:source");
76 :
77 0 : g_string_free(buffer, TRUE);
78 0 : free_xml(copy);
79 0 : return digest;
80 : }
81 :
82 : /*!
83 : * \brief Calculate and return v2 digest of XML tree
84 : *
85 : * \param[in] source Root of XML to digest
86 : * \param[in] do_filter Whether to filter certain XML attributes
87 : *
88 : * \return Newly allocated string containing digest
89 : */
90 : static char *
91 0 : calculate_xml_digest_v2(const xmlNode *source, gboolean do_filter)
92 : {
93 0 : char *digest = NULL;
94 0 : GString *buf = g_string_sized_new(1024);
95 :
96 0 : crm_trace("Begin digest %s", do_filter?"filtered":"");
97 :
98 0 : pcmk__xml_string(source, (do_filter? pcmk__xml_fmt_filtered : 0), buf, 0);
99 0 : digest = crm_md5sum(buf->str);
100 :
101 0 : pcmk__if_tracing(
102 : {
103 : char *trace_file = crm_strdup_printf("%s/digest-%s",
104 : pcmk__get_tmpdir(), digest);
105 :
106 : crm_trace("Saving %s.%s.%s to %s",
107 : crm_element_value(source, PCMK_XA_ADMIN_EPOCH),
108 : crm_element_value(source, PCMK_XA_EPOCH),
109 : crm_element_value(source, PCMK_XA_NUM_UPDATES),
110 : trace_file);
111 : save_xml_to_file(source, "digest input", trace_file);
112 : free(trace_file);
113 : },
114 : {}
115 : );
116 0 : crm_trace("End digest");
117 0 : g_string_free(buf, TRUE);
118 0 : return digest;
119 : }
120 :
121 : /*!
122 : * \brief Calculate and return digest of XML tree, suitable for storing on disk
123 : *
124 : * \param[in] input Root of XML to digest
125 : *
126 : * \return Newly allocated string containing digest
127 : */
128 : char *
129 0 : calculate_on_disk_digest(xmlNode *input)
130 : {
131 : /* Always use the v1 format for on-disk digests
132 : * a) it's a compatibility nightmare
133 : * b) we only use this once at startup, all other
134 : * invocations are in a separate child process
135 : */
136 0 : return calculate_xml_digest_v1(input, FALSE, FALSE);
137 : }
138 :
139 : /*!
140 : * \brief Calculate and return digest of XML operation
141 : *
142 : * \param[in] input Root of XML to digest
143 : * \param[in] version Unused
144 : *
145 : * \return Newly allocated string containing digest
146 : */
147 : char *
148 0 : calculate_operation_digest(xmlNode *input, const char *version)
149 : {
150 : /* We still need the sorting for operation digests */
151 0 : return calculate_xml_digest_v1(input, TRUE, FALSE);
152 : }
153 :
154 : /*!
155 : * \brief Calculate and return digest of XML tree
156 : *
157 : * \param[in] input Root of XML to digest
158 : * \param[in] sort Whether to sort XML before calculating digest
159 : * \param[in] do_filter Whether to filter certain XML attributes
160 : * \param[in] version CRM feature set version (used to select v1/v2 digest)
161 : *
162 : * \return Newly allocated string containing digest
163 : */
164 : char *
165 0 : calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
166 : gboolean do_filter, const char *version)
167 : {
168 : /*
169 : * @COMPAT digests (on-disk or in diffs/patchsets) created <1.1.4;
170 : * removing this affects even full-restart upgrades from old versions
171 : *
172 : * The sorting associated with v1 digest creation accounted for 23% of
173 : * the CIB manager's CPU usage on the server. v2 drops this.
174 : *
175 : * The filtering accounts for an additional 2.5% and we may want to
176 : * remove it in future.
177 : *
178 : * v2 also uses the xmlBuffer contents directly to avoid additional copying
179 : */
180 0 : if (version == NULL || compare_version("3.0.5", version) > 0) {
181 0 : crm_trace("Using v1 digest algorithm for %s",
182 : pcmk__s(version, "unknown feature set"));
183 0 : return calculate_xml_digest_v1(input, sort, do_filter);
184 : }
185 0 : crm_trace("Using v2 digest algorithm for %s",
186 : pcmk__s(version, "unknown feature set"));
187 0 : return calculate_xml_digest_v2(input, do_filter);
188 : }
189 :
190 : /*!
191 : * \internal
192 : * \brief Check whether calculated digest of given XML matches expected digest
193 : *
194 : * \param[in] input Root of XML tree to digest
195 : * \param[in] expected Expected digest in on-disk format
196 : *
197 : * \return true if digests match, false on mismatch or error
198 : */
199 : bool
200 0 : pcmk__verify_digest(xmlNode *input, const char *expected)
201 : {
202 0 : char *calculated = NULL;
203 : bool passed;
204 :
205 0 : if (input != NULL) {
206 0 : calculated = calculate_on_disk_digest(input);
207 0 : if (calculated == NULL) {
208 0 : crm_perror(LOG_ERR, "Could not calculate digest for comparison");
209 0 : return false;
210 : }
211 : }
212 0 : passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
213 0 : if (passed) {
214 0 : crm_trace("Digest comparison passed: %s", calculated);
215 : } else {
216 0 : crm_err("Digest comparison failed: expected %s, calculated %s",
217 : expected, calculated);
218 : }
219 0 : free(calculated);
220 0 : return passed;
221 : }
222 :
223 : /*!
224 : * \internal
225 : * \brief Check whether an XML attribute should be excluded from CIB digests
226 : *
227 : * \param[in] name XML attribute name
228 : *
229 : * \return true if XML attribute should be excluded from CIB digest calculation
230 : */
231 : bool
232 0 : pcmk__xa_filterable(const char *name)
233 : {
234 : static const char *filter[] = {
235 : PCMK_XA_CRM_DEBUG_ORIGIN,
236 : PCMK_XA_CIB_LAST_WRITTEN,
237 : PCMK_XA_UPDATE_ORIGIN,
238 : PCMK_XA_UPDATE_CLIENT,
239 : PCMK_XA_UPDATE_USER,
240 : };
241 :
242 0 : for (int i = 0; i < PCMK__NELEM(filter); i++) {
243 0 : if (strcmp(name, filter[i]) == 0) {
244 0 : return true;
245 : }
246 : }
247 0 : return false;
248 : }
249 :
250 : char *
251 0 : crm_md5sum(const char *buffer)
252 : {
253 0 : int lpc = 0, len = 0;
254 0 : char *digest = NULL;
255 : unsigned char raw_digest[MD5_DIGEST_SIZE];
256 :
257 0 : if (buffer == NULL) {
258 0 : buffer = "";
259 : }
260 0 : len = strlen(buffer);
261 :
262 0 : crm_trace("Beginning digest of %d bytes", len);
263 0 : digest = malloc(2 * MD5_DIGEST_SIZE + 1);
264 0 : if (digest) {
265 0 : md5_buffer(buffer, len, raw_digest);
266 0 : for (lpc = 0; lpc < MD5_DIGEST_SIZE; lpc++) {
267 0 : sprintf(digest + (2 * lpc), "%02x", raw_digest[lpc]);
268 : }
269 0 : digest[(2 * MD5_DIGEST_SIZE)] = 0;
270 0 : crm_trace("Digest %s.", digest);
271 :
272 : } else {
273 0 : crm_err("Could not create digest");
274 : }
275 0 : return digest;
276 : }
277 :
278 : // Return true if a is an attribute that should be filtered
279 : static bool
280 0 : should_filter_for_digest(xmlAttrPtr a, void *user_data)
281 : {
282 0 : if (strncmp((const char *) a->name, CRM_META "_",
283 : sizeof(CRM_META " ") - 1) == 0) {
284 0 : return true;
285 : }
286 0 : return pcmk__str_any_of((const char *) a->name,
287 : PCMK_XA_ID,
288 : PCMK_XA_CRM_FEATURE_SET,
289 : PCMK__XA_OP_DIGEST,
290 : PCMK__META_ON_NODE,
291 : PCMK__META_ON_NODE_UUID,
292 : "pcmk_external_ip",
293 : NULL);
294 : }
295 :
296 : /*!
297 : * \internal
298 : * \brief Remove XML attributes not needed for operation digest
299 : *
300 : * \param[in,out] param_set XML with operation parameters
301 : */
302 : void
303 0 : pcmk__filter_op_for_digest(xmlNode *param_set)
304 : {
305 0 : char *key = NULL;
306 0 : char *timeout = NULL;
307 0 : guint interval_ms = 0;
308 :
309 0 : if (param_set == NULL) {
310 0 : return;
311 : }
312 :
313 : /* Timeout is useful for recurring operation digests, so grab it before
314 : * removing meta-attributes
315 : */
316 0 : key = crm_meta_name(PCMK_META_INTERVAL);
317 0 : if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
318 0 : interval_ms = 0;
319 : }
320 0 : free(key);
321 0 : key = NULL;
322 0 : if (interval_ms != 0) {
323 0 : key = crm_meta_name(PCMK_META_TIMEOUT);
324 0 : timeout = crm_element_value_copy(param_set, key);
325 : }
326 :
327 : // Remove all CRM_meta_* attributes and certain other attributes
328 0 : pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL);
329 :
330 : // Add timeout back for recurring operation digests
331 0 : if (timeout != NULL) {
332 0 : crm_xml_add(param_set, key, timeout);
333 : }
334 0 : free(timeout);
335 0 : free(key);
336 : }
|