1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/lib/pgBackRest/Common/Ini.pm
Cynthia Shang b03c26968a Repository encryption support.
Contributed by Cynthia Shang.
2017-11-06 12:51:12 -05:00

874 lines
30 KiB
Perl

####################################################################################################################################
# COMMON INI MODULE
####################################################################################################################################
package pgBackRest::Common::Ini;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';
use Digest::SHA;
use Exporter qw(import);
our @EXPORT = qw();
use Fcntl qw(:mode O_WRONLY O_CREAT O_TRUNC);
use File::Basename qw(dirname basename);
use IO::Handle;
use JSON::PP;
use Storable qw(dclone);
use pgBackRest::Common::Exception;
use pgBackRest::Common::Log;
use pgBackRest::Common::String;
use pgBackRest::Version;
####################################################################################################################################
# Boolean constants
####################################################################################################################################
use constant INI_TRUE => JSON::PP::true;
push @EXPORT, qw(INI_TRUE);
use constant INI_FALSE => JSON::PP::false;
push @EXPORT, qw(INI_FALSE);
####################################################################################################################################
# Ini control constants
####################################################################################################################################
use constant INI_SECTION_BACKREST => 'backrest';
push @EXPORT, qw(INI_SECTION_BACKREST);
use constant INI_KEY_CHECKSUM => 'backrest-checksum';
push @EXPORT, qw(INI_KEY_CHECKSUM);
use constant INI_KEY_FORMAT => 'backrest-format';
push @EXPORT, qw(INI_KEY_FORMAT);
use constant INI_KEY_VERSION => 'backrest-version';
push @EXPORT, qw(INI_KEY_VERSION);
use constant INI_SECTION_CIPHER => 'cipher';
push @EXPORT, qw(INI_SECTION_CIPHER);
use constant INI_KEY_CIPHER_PASS => 'cipher-pass';
push @EXPORT, qw(INI_KEY_CIPHER_PASS);
####################################################################################################################################
# Ini file copy extension
####################################################################################################################################
use constant INI_COPY_EXT => '.copy';
push @EXPORT, qw(INI_COPY_EXT);
####################################################################################################################################
# Ini sort orders
####################################################################################################################################
use constant INI_SORT_FORWARD => 'forward';
push @EXPORT, qw(INI_SORT_FORWARD);
use constant INI_SORT_REVERSE => 'reverse';
push @EXPORT, qw(INI_SORT_REVERSE);
use constant INI_SORT_NONE => 'none';
push @EXPORT, qw(INI_SORT_NONE);
####################################################################################################################################
# new()
####################################################################################################################################
sub new
{
my $class = shift; # Class name
# Create the class hash
my $self = {};
bless $self, $class;
# Load Storage::Helper module
require pgBackRest::Storage::Helper;
pgBackRest::Storage::Helper->import();
# Assign function parameters, defaults, and log debug info
(
my $strOperation,
$self->{strFileName},
my $bLoad,
my $strContent,
$self->{oStorage},
$self->{iInitFormat},
$self->{strInitVersion},
my $bIgnoreMissing,
$self->{strCipherPass}, # Passphrase to read/write the file
my $strCipherPassSub, # Passphrase to read/write subsequent files
) =
logDebugParam
(
__PACKAGE__ . '->new', \@_,
{name => 'strFileName', trace => true},
{name => 'bLoad', optional => true, default => true, trace => true},
{name => 'strContent', optional => true, trace => true},
{name => 'oStorage', optional => true, default => storageLocal(), trace => true},
{name => 'iInitFormat', optional => true, default => BACKREST_FORMAT, trace => true},
{name => 'strInitVersion', optional => true, default => BACKREST_VERSION, trace => true},
{name => 'bIgnoreMissing', optional => true, default => false, trace => true},
{name => 'strCipherPass', optional => true, trace => true},
{name => 'strCipherPassSub', optional => true, trace => true},
);
if (defined($self->{oStorage}->cipherPassUser()) && !defined($self->{strCipherPass}))
{
confess &log(ERROR, 'passphrase is required when storage is encrypted', ERROR_CIPHER);
}
# Set changed to false
$self->{bModified} = false;
# Set exists to false
$self->{bExists} = false;
# Load the file if requested
if ($bLoad)
{
$self->load($bIgnoreMissing);
}
# Load from a string if provided
elsif (defined($strContent))
{
$self->{oContent} = iniParse($strContent);
$self->headerCheck();
}
# Initialize if not loading the file and not loading from string or if a load was attempted and the file does not exist
if (!$self->{bExists} && !defined($strContent))
{
$self->numericSet(INI_SECTION_BACKREST, INI_KEY_FORMAT, undef, $self->{iInitFormat});
$self->set(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion});
# Determine if the passphrase section should be set
if (defined($self->{strCipherPass}) && defined($strCipherPassSub))
{
$self->set(INI_SECTION_CIPHER, INI_KEY_CIPHER_PASS, undef, $strCipherPassSub);
}
elsif ((defined($self->{strCipherPass}) && !defined($strCipherPassSub)) ||
(!defined($self->{strCipherPass}) && defined($strCipherPassSub)))
{
confess &log(ASSERT, 'a user passphrase and sub passphrase are both required when encrypting');
}
}
return $self;
}
####################################################################################################################################
# loadVersion() - load a version (main or copy) of the ini file
####################################################################################################################################
sub loadVersion
{
my $self = shift;
my $bCopy = shift;
my $bIgnoreError = shift;
# Make sure the file encryption setting is valid for the repo
if ($self->{oStorage}->encryptionValid($self->{oStorage}->encrypted($self->{strFileName} . ($bCopy ? INI_COPY_EXT : ''),
{bIgnoreMissing => $bIgnoreError})))
{
# Load main
my $rstrContent = $self->{oStorage}->get(
$self->{oStorage}->openRead($self->{strFileName} . ($bCopy ? INI_COPY_EXT : ''),
{bIgnoreMissing => $bIgnoreError, strCipherPass => $self->{strCipherPass}}));
# If the file exists then attempt to parse it
if (defined($rstrContent))
{
my $rhContent = iniParse($$rstrContent, {bIgnoreInvalid => $bIgnoreError});
# If the content is valid then check the header
if (defined($rhContent))
{
$self->{oContent} = $rhContent;
# If the header is invalid then undef content
if (!$self->headerCheck({bIgnoreInvalid => $bIgnoreError}))
{
delete($self->{oContent});
}
}
}
}
else
{
confess &log(ERROR, "unable to parse '$self->{strFileName}" . ($bCopy ? INI_COPY_EXT : '') . "'" .
"\nHINT: Is or was the repo encrypted?", ERROR_CIPHER);
}
return defined($self->{oContent});
}
####################################################################################################################################
# load() - load the ini
####################################################################################################################################
sub load
{
my $self = shift;
my $bIgnoreMissing = shift;
# If main was not loaded then try the copy
if (!$self->loadVersion(false, true))
{
if (!$self->loadVersion(true, true))
{
return if $bIgnoreMissing;
confess &log(ERROR, "unable to open $self->{strFileName} or $self->{strFileName}" . INI_COPY_EXT, ERROR_FILE_MISSING);
}
}
$self->{bExists} = true;
}
####################################################################################################################################
# headerCheck() - check that version and checksum in header are as expected
####################################################################################################################################
sub headerCheck
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$bIgnoreInvalid,
) =
logDebugParam
(
__PACKAGE__ . '->headerCheck', \@_,
{name => 'bIgnoreInvalid', optional => true, default => false, trace => true},
);
# Eval so exceptions can be ignored on bIgnoreInvalid
my $bValid = true;
eval
{
# Make sure the ini is valid by testing checksum
my $strChecksum = $self->get(INI_SECTION_BACKREST, INI_KEY_CHECKSUM, undef, false);
my $strTestChecksum = $self->hash();
if (!defined($strChecksum) || $strChecksum ne $strTestChecksum)
{
confess &log(ERROR,
"invalid checksum in '$self->{strFileName}', expected '${strTestChecksum}' but found " .
(defined($strChecksum) ? "'${strChecksum}'" : '[undef]'),
ERROR_CHECKSUM);
}
# Make sure that the format is current, otherwise error
my $iFormat = $self->get(INI_SECTION_BACKREST, INI_KEY_FORMAT, undef, false, 0);
if ($iFormat != $self->{iInitFormat})
{
confess &log(ERROR,
"invalid format in '$self->{strFileName}', expected $self->{iInitFormat} but found ${iFormat}", ERROR_FORMAT);
}
# Check if the version has changed
if (!$self->test(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion}))
{
$self->set(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion});
}
return true;
}
or do
{
# Confess the error if it should not be ignored
if (!$bIgnoreInvalid)
{
confess $EVAL_ERROR;
}
# Return false when errors are ignored
$bValid = false;
};
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bValid', value => $bValid, trace => true}
);
}
####################################################################################################################################
# iniParse() - parse from standard INI format to a hash.
####################################################################################################################################
push @EXPORT, qw(iniParse);
sub iniParse
{
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strContent,
$bRelaxed,
$bIgnoreInvalid,
) =
logDebugParam
(
__PACKAGE__ . '::iniParse', \@_,
{name => 'strContent', required => false, trace => true},
{name => 'bRelaxed', optional => true, default => false, trace => true},
{name => 'bIgnoreInvalid', optional => true, default => false, trace => true},
);
# Ini content
my $oContent = undef;
my $strSection;
# Create the JSON object
my $oJSON = JSON::PP->new()->allow_nonref();
# Eval so exceptions can be ignored on bIgnoreInvalid
eval
{
# Read the INI file
foreach my $strLine (split("\n", defined($strContent) ? $strContent : ''))
{
$strLine = trim($strLine);
# Skip lines that are blank or comments
if ($strLine ne '' && $strLine !~ '^[ ]*#.*')
{
# Get the section
if (index($strLine, '[') == 0)
{
$strSection = substr($strLine, 1, length($strLine) - 2);
}
else
{
if (!defined($strSection))
{
confess &log(ERROR, "key/value pair '${strLine}' found outside of a section", ERROR_CONFIG);
}
# Get key and value
my $iIndex = index($strLine, '=');
if ($iIndex == -1)
{
confess &log(ERROR, "unable to find '=' in '${strLine}'", ERROR_CONFIG);
}
my $strKey = substr($strLine, 0, $iIndex);
my $strValue = substr($strLine, $iIndex + 1);
# If relaxed then read the value directly
if ($bRelaxed)
{
if (defined($oContent->{$strSection}{$strKey}))
{
if (ref($oContent->{$strSection}{$strKey}) ne 'ARRAY')
{
$oContent->{$strSection}{$strKey} = [$oContent->{$strSection}{$strKey}];
}
push(@{$oContent->{$strSection}{$strKey}}, $strValue);
}
else
{
$oContent->{$strSection}{$strKey} = $strValue;
}
}
# Else read the value as stricter JSON
else
{
${$oContent}{$strSection}{$strKey} = $oJSON->decode($strValue);
}
}
}
}
# Error if the file is empty
if (!($bRelaxed || defined($oContent)))
{
confess &log(ERROR, 'no key/value pairs found', ERROR_CONFIG);
}
return true;
}
or do
{
# Confess the error if it should not be ignored
if (!$bIgnoreInvalid)
{
confess $EVAL_ERROR;
}
# Undef content when errors are ignored
undef($oContent);
};
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'oContent', value => $oContent, trace => true}
);
}
####################################################################################################################################
# save() - save the file.
####################################################################################################################################
sub save
{
my $self = shift;
# Save only if modified
if ($self->{bModified})
{
# Calculate the hash
$self->hash();
# Save the file
$self->{oStorage}->put($self->{strFileName}, iniRender($self->{oContent}), {strCipherPass => $self->{strCipherPass}});
$self->{oStorage}->pathSync(dirname($self->{strFileName}));
$self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}),
{strCipherPass => $self->{strCipherPass}});
$self->{oStorage}->pathSync(dirname($self->{strFileName}));
$self->{bModified} = false;
# Indicate the file now exists
$self->{bExists} = true;
# File was saved
return true;
}
# File was not saved
return false;
}
####################################################################################################################################
# saveCopy - save only a copy of the file.
####################################################################################################################################
sub saveCopy
{
my $self = shift;
if ($self->{oStorage}->exists($self->{strFileName}))
{
confess &log(ASSERT, "cannot save copy only when '$self->{strFileName}' exists");
}
$self->hash();
$self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}),
{strCipherPass => $self->{strCipherPass}});
}
####################################################################################################################################
# iniRender() - render hash to standard INI format.
####################################################################################################################################
push @EXPORT, qw(iniRender);
sub iniRender
{
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$oContent,
$bRelaxed,
) =
logDebugParam
(
__PACKAGE__ . '::iniRender', \@_,
{name => 'oContent', trace => true},
{name => 'bRelaxed', default => false, trace => true},
);
# Open the ini file for writing
my $strContent = '';
my $bFirst = true;
# Create the JSON object canonical so that fields are alpha ordered to pass unit tests
my $oJSON = JSON::PP->new()->canonical()->allow_nonref();
# Write the INI file
foreach my $strSection (sort(keys(%$oContent)))
{
# Add a linefeed between sections
if (!$bFirst)
{
$strContent .= "\n";
}
# Write the section
$strContent .= "[${strSection}]\n";
# Iterate through all keys in the section
foreach my $strKey (sort(keys(%{$oContent->{$strSection}})))
{
# If the value is a hash then convert it to JSON, otherwise store as is
my $strValue = ${$oContent}{$strSection}{$strKey};
# If relaxed then store as old-style config
if ($bRelaxed)
{
# If the value is an array then save each element to a separate key/value pair
if (ref($strValue) eq 'ARRAY')
{
foreach my $strArrayValue (@{$strValue})
{
$strContent .= "${strKey}=${strArrayValue}\n";
}
}
# Else write a standard key/value pair
else
{
$strContent .= "${strKey}=${strValue}\n";
}
}
# Else write as stricter JSON
else
{
$strContent .= "${strKey}=" . $oJSON->encode($strValue) . "\n";
}
}
$bFirst = false;
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strContent', value => $strContent, trace => true}
);
}
####################################################################################################################################
# hash() - generate hash for the manifest.
####################################################################################################################################
sub hash
{
my $self = shift;
# Remove the old checksum
delete($self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM});
# Calculate the checksum
my $oSHA = Digest::SHA->new('sha1');
my $oJSON = JSON::PP->new()->canonical()->allow_nonref();
$oSHA->add($oJSON->encode($self->{oContent}));
# Set the new checksum
$self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM} = $oSHA->hexdigest();
return $self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM};
}
####################################################################################################################################
# get() - get a value.
####################################################################################################################################
sub get
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bRequired = shift;
my $oDefault = shift;
# Parameter constraints
if (!defined($strSection))
{
confess &log(ASSERT, 'strSection is required');
}
if (defined($strSubKey) && !defined($strKey))
{
confess &log(ASSERT, "strKey is required when strSubKey '${strSubKey}' is requested");
}
# Get the result
my $oResult = $self->{oContent}->{$strSection};
if (defined($strKey) && defined($oResult))
{
$oResult = $oResult->{$strKey};
if (defined($strSubKey) && defined($oResult))
{
$oResult = $oResult->{$strSubKey};
}
}
# When result is not defined
if (!defined($oResult))
{
# Error if a result is required
if (!defined($bRequired) || $bRequired)
{
confess &log(ASSERT, "strSection '$strSection'" . (defined($strKey) ? ", strKey '$strKey'" : '') .
(defined($strSubKey) ? ", strSubKey '$strSubKey'" : '') . ' is required but not defined');
}
# Return default if specified
if (defined($oDefault))
{
return $oDefault;
}
}
return $oResult
}
####################################################################################################################################
# boolGet() - get a boolean value.
####################################################################################################################################
sub boolGet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bRequired = shift;
my $bDefault = shift;
return $self->get(
$strSection, $strKey, $strSubKey, $bRequired,
defined($bDefault) ? ($bDefault ? INI_TRUE : INI_FALSE) : undef) ? true : false;
}
####################################################################################################################################
# numericGet() - get a numeric value.
####################################################################################################################################
sub numericGet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bRequired = shift;
my $nDefault = shift;
return $self->get($strSection, $strKey, $strSubKey, $bRequired, defined($nDefault) ? $nDefault + 0 : undef) + 0;
}
####################################################################################################################################
# set - set a value.
####################################################################################################################################
sub set
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $oValue = shift;
# Parameter constraints
if (!(defined($strSection) && defined($strKey)))
{
confess &log(ASSERT, 'strSection and strKey are required');
}
my $oCurrentValue;
if (defined($strSubKey))
{
$oCurrentValue = \$self->{oContent}{$strSection}{$strKey}{$strSubKey};
}
else
{
$oCurrentValue = \$self->{oContent}{$strSection}{$strKey};
}
if (!defined($$oCurrentValue) ||
defined($oCurrentValue) != defined($oValue) ||
${dclone($oCurrentValue)} ne ${dclone(\$oValue)})
{
$$oCurrentValue = $oValue;
if (!$self->{bModified})
{
$self->{bModified} = true;
}
return true;
}
return false;
}
####################################################################################################################################
# boolSet - set a boolean value.
####################################################################################################################################
sub boolSet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bValue = shift;
$self->set($strSection, $strKey, $strSubKey, $bValue ? INI_TRUE : INI_FALSE);
}
####################################################################################################################################
# numericSet - set a numeric value.
####################################################################################################################################
sub numericSet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $nValue = shift;
$self->set($strSection, $strKey, $strSubKey, defined($nValue) ? $nValue + 0 : undef);
}
####################################################################################################################################
# remove - remove a value.
####################################################################################################################################
sub remove
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
# Test if the value exists
if ($self->test($strSection, $strKey, $strSubKey))
{
# Remove a subkey
if (defined($strSubKey))
{
delete($self->{oContent}{$strSection}{$strKey}{$strSubKey});
}
# Remove a key
if (defined($strKey))
{
if (!defined($strSubKey))
{
delete($self->{oContent}{$strSection}{$strKey});
}
# Remove the section if it is now empty
if (keys(%{$self->{oContent}{$strSection}}) == 0)
{
delete($self->{oContent}{$strSection});
}
}
# Remove a section
if (!defined($strKey))
{
delete($self->{oContent}{$strSection});
}
# Record changes
if (!$self->{bModified})
{
$self->{bModified} = true;
}
return true;
}
return false;
}
####################################################################################################################################
# keys - get the list of keys in a section.
####################################################################################################################################
sub keys
{
my $self = shift;
my $strSection = shift;
my $strSortOrder = shift;
if ($self->test($strSection))
{
if (!defined($strSortOrder) || $strSortOrder eq INI_SORT_FORWARD)
{
return (sort(keys(%{$self->get($strSection)})));
}
elsif ($strSortOrder eq INI_SORT_REVERSE)
{
return (sort {$b cmp $a} (keys(%{$self->get($strSection)})));
}
elsif ($strSortOrder eq INI_SORT_NONE)
{
return (keys(%{$self->get($strSection)}));
}
else
{
confess &log(ASSERT, "invalid strSortOrder '${strSortOrder}'");
}
}
my @stryEmptyArray;
return @stryEmptyArray;
}
####################################################################################################################################
# test - test a value.
#
# Test a value to see if it equals the supplied test value. If no test value is given, tests that the section, key, or subkey
# is defined.
####################################################################################################################################
sub test
{
my $self = shift;
my $strSection = shift;
my $strValue = shift;
my $strSubValue = shift;
my $strTest = shift;
# Get the value
my $strResult = $self->get($strSection, $strValue, $strSubValue, false);
# Is there a result
if (defined($strResult))
{
# Is there a value to test against?
if (defined($strTest))
{
return $strResult eq $strTest ? true : false;
}
return true;
}
return false;
}
####################################################################################################################################
# boolTest - test a boolean value, see test().
####################################################################################################################################
sub boolTest
{
my $self = shift;
my $strSection = shift;
my $strValue = shift;
my $strSubValue = shift;
my $bTest = shift;
return $self->test($strSection, $strValue, $strSubValue, defined($bTest) ? ($bTest ? INI_TRUE : INI_FALSE) : undef);
}
####################################################################################################################################
# cipherPassSub - gets the passphrase (if it exists) used to read/write subsequent files
####################################################################################################################################
sub cipherPassSub
{
my $self = shift;
return $self->get(INI_SECTION_CIPHER, INI_KEY_CIPHER_PASS, undef, false);
}
####################################################################################################################################
# Properties.
####################################################################################################################################
sub modified {shift->{bModified}} # Has the data been modified since last load/save?
sub exists {shift->{bExists}} # Is the data persisted to file?
sub cipherPass {shift->{strCipherPass}} # Return passphrase (will be undef if repo not encrypted)
1;