/*
* 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_strdup_printf("%s/digest-%s",
crm_get_tmpdir(), 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)
{
/*
* @COMPAT digests (on-disk or in diffs/patchsets) created <1.1.4;
* removing this affects even full-restart upgrades from old versions
*
* The sorting associated with v1 digest creation accounted for 23% of
* the CIB manager'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;
}