| /* |
| * Copyright (C) 2015 Andrew Beekhof <andrew@beekhof.net> |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| */ |
| |
| #include <crm_internal.h> |
| |
| #include <stdio.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <stdlib.h> |
| |
| #include <crm/crm.h> |
| #include <crm/msg_xml.h> |
| #include <crm/common/xml.h> |
| |
| #define BEST_EFFORT_STATUS 0 |
| |
| /*! |
| * \brief Dump XML in a format used with v1 digests |
| * |
| * \param[in] an_xml_node Root of XML to dump |
| * |
| * \return Newly allocated buffer containing dumped XML |
| */ |
| static char * |
| dump_xml_for_digest(xmlNode * an_xml_node) |
| { |
| char *buffer = NULL; |
| int offset = 0, max = 0; |
| |
| /* for compatibility with the old result which is used for v1 digests */ |
| crm_buffer_add_char(&buffer, &offset, &max, ' '); |
| crm_xml_dump(an_xml_node, 0, &buffer, &offset, &max, 0); |
| crm_buffer_add_char(&buffer, &offset, &max, '\n'); |
| |
| return buffer; |
| } |
| |
| /*! |
| * \brief Calculate and return v1 digest of XML tree |
| * |
| * \param[in] input Root of XML to digest |
| * \param[in] sort Whether to sort the XML before calculating digest |
| * \param[in] ignored Not used |
| * |
| * \return Newly allocated string containing digest |
| * \note Example return value: "c048eae664dba840e1d2060f00299e9d" |
| */ |
| static char * |
| calculate_xml_digest_v1(xmlNode * input, gboolean sort, gboolean ignored) |
| { |
| char *digest = NULL; |
| char *buffer = NULL; |
| xmlNode *copy = NULL; |
| |
| if (sort) { |
| crm_trace("Sorting xml..."); |
| copy = sorted_xml(input, NULL, TRUE); |
| crm_trace("Done"); |
| input = copy; |
| } |
| |
| buffer = dump_xml_for_digest(input); |
| CRM_CHECK(buffer != NULL && strlen(buffer) > 0, free_xml(copy); |
| free(buffer); |
| return NULL); |
| |
| digest = crm_md5sum(buffer); |
| crm_log_xml_trace(input, "digest:source"); |
| |
| free(buffer); |
| free_xml(copy); |
| return digest; |
| } |
| |
| /*! |
| * \brief Calculate and return v2 digest of XML tree |
| * |
| * \param[in] source Root of XML to digest |
| * \param[in] do_filter Whether to filter certain XML attributes |
| * |
| * \return Newly allocated string containing digest |
| */ |
| static char * |
| calculate_xml_digest_v2(xmlNode * source, gboolean do_filter) |
| { |
| char *digest = NULL; |
| char *buffer = NULL; |
| int offset, max; |
| |
| static struct qb_log_callsite *digest_cs = NULL; |
| |
| crm_trace("Begin digest %s", do_filter?"filtered":""); |
| if (do_filter && BEST_EFFORT_STATUS) { |
| /* Exclude the status calculation from the digest |
| * |
| * This doesn't mean it won't be sync'd, we just won't be paranoid |
| * about it being an _exact_ copy |
| * |
| * We don't need it to be exact, since we throw it away and regenerate |
| * from our peers whenever a new DC is elected anyway |
| * |
| * Importantly, this reduces the amount of XML to copy+export as |
| * well as the amount of data for MD5 needs to operate on |
| */ |
| |
| } else { |
| crm_xml_dump(source, do_filter ? xml_log_option_filtered : 0, &buffer, &offset, &max, 0); |
| } |
| |
| CRM_ASSERT(buffer != NULL); |
| digest = crm_md5sum(buffer); |
| |
| if (digest_cs == NULL) { |
| digest_cs = qb_log_callsite_get(__func__, __FILE__, "cib-digest", LOG_TRACE, __LINE__, |
| crm_trace_nonlog); |
| } |
| if (digest_cs && digest_cs->targets) { |
| char *trace_file = crm_concat("/tmp/digest", digest, '-'); |
| |
| crm_trace("Saving %s.%s.%s to %s", |
| crm_element_value(source, XML_ATTR_GENERATION_ADMIN), |
| crm_element_value(source, XML_ATTR_GENERATION), |
| crm_element_value(source, XML_ATTR_NUMUPDATES), trace_file); |
| save_xml_to_file(source, "digest input", trace_file); |
| free(trace_file); |
| } |
| |
| free(buffer); |
| crm_trace("End digest"); |
| return digest; |
| } |
| |
| /*! |
| * \brief Calculate and return digest of XML tree, suitable for storing on disk |
| * |
| * \param[in] input Root of XML to digest |
| * |
| * \return Newly allocated string containing digest |
| */ |
| char * |
| calculate_on_disk_digest(xmlNode * input) |
| { |
| /* Always use the v1 format for on-disk digests |
| * a) it's a compatibility nightmare |
| * b) we only use this once at startup, all other |
| * invocations are in a separate child process |
| */ |
| return calculate_xml_digest_v1(input, FALSE, FALSE); |
| } |
| |
| /*! |
| * \brief Calculate and return digest of XML operation |
| * |
| * \param[in] input Root of XML to digest |
| * \param[in] version Not used |
| * |
| * \return Newly allocated string containing digest |
| */ |
| char * |
| calculate_operation_digest(xmlNode *input, const char *version) |
| { |
| /* We still need the sorting for operation digests */ |
| return calculate_xml_digest_v1(input, TRUE, FALSE); |
| } |
| |
| /*! |
| * \brief Calculate and return digest of XML tree |
| * |
| * \param[in] input Root of XML to digest |
| * \param[in] sort Whether to sort XML before calculating digest |
| * \param[in] do_filter Whether to filter certain XML attributes |
| * \param[in] version CRM feature set version (used to select v1/v2 digest) |
| * |
| * \return Newly allocated string containing digest |
| */ |
| char * |
| calculate_xml_versioned_digest(xmlNode * input, gboolean sort, gboolean do_filter, |
| const char *version) |
| { |
| /* |
| * The sorting associated with v1 digest creation accounted for 23% of |
| * the CIB's CPU usage on the server. v2 drops this. |
| * |
| * The filtering accounts for an additional 2.5% and we may want to |
| * remove it in future. |
| * |
| * v2 also uses the xmlBuffer contents directly to avoid additional copying |
| */ |
| if (version == NULL || compare_version("3.0.5", version) > 0) { |
| crm_trace("Using v1 digest algorithm for %s", crm_str(version)); |
| return calculate_xml_digest_v1(input, sort, do_filter); |
| } |
| crm_trace("Using v2 digest algorithm for %s", crm_str(version)); |
| return calculate_xml_digest_v2(input, do_filter); |
| } |
| |
| /*! |
| * \internal |
| * \brief Return whether calculated digest of XML tree matches expected digest |
| * |
| * \param[in] input Root of XML to digest |
| * \param[in] expected Expected digest in on-disk format |
| * |
| * \return TRUE if digests match, FALSE otherwise or on error |
| */ |
| gboolean |
| crm_digest_verify(xmlNode *input, const char *expected) |
| { |
| char *calculated = NULL; |
| gboolean passed; |
| |
| if (input != NULL) { |
| calculated = calculate_on_disk_digest(input); |
| if (calculated == NULL) { |
| crm_perror(LOG_ERR, "Could not calculate digest for comparison"); |
| return FALSE; |
| } |
| } |
| passed = safe_str_eq(expected, calculated); |
| if (passed) { |
| crm_trace("Digest comparison passed: %s", calculated); |
| } else { |
| crm_err("Digest comparison failed: expected %s, calculated %s", |
| expected, calculated); |
| } |
| free(calculated); |
| return passed; |
| } |