2017-06-12 16:52:32 +02:00
|
|
|
####################################################################################################################################
|
|
|
|
# S3 Request
|
|
|
|
####################################################################################################################################
|
|
|
|
package pgBackRest::Storage::S3::Request;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings FATAL => qw(all);
|
|
|
|
use Carp qw(confess);
|
|
|
|
use English '-no_match_vars';
|
|
|
|
|
|
|
|
use Exporter qw(import);
|
|
|
|
our @EXPORT = qw();
|
|
|
|
use IO::Socket::SSL;
|
|
|
|
|
|
|
|
use pgBackRest::Common::Exception;
|
|
|
|
use pgBackRest::Common::Http::Client;
|
|
|
|
use pgBackRest::Common::Http::Common;
|
|
|
|
use pgBackRest::Common::Io::Base;
|
|
|
|
use pgBackRest::Common::Log;
|
|
|
|
use pgBackRest::Common::String;
|
|
|
|
use pgBackRest::Common::Xml;
|
2018-06-11 20:52:26 +02:00
|
|
|
use pgBackRest::LibC qw(:crypto);
|
2017-06-12 16:52:32 +02:00
|
|
|
use pgBackRest::Storage::S3::Auth;
|
|
|
|
|
|
|
|
####################################################################################################################################
|
|
|
|
# Constants
|
|
|
|
####################################################################################################################################
|
|
|
|
use constant HTTP_VERB_GET => 'GET';
|
|
|
|
push @EXPORT, qw(HTTP_VERB_GET);
|
|
|
|
use constant HTTP_VERB_POST => 'POST';
|
|
|
|
push @EXPORT, qw(HTTP_VERB_POST);
|
|
|
|
use constant HTTP_VERB_PUT => 'PUT';
|
|
|
|
push @EXPORT, qw(HTTP_VERB_PUT);
|
|
|
|
|
|
|
|
use constant S3_HEADER_CONTENT_LENGTH => 'content-length';
|
|
|
|
push @EXPORT, qw(S3_HEADER_CONTENT_LENGTH);
|
|
|
|
use constant S3_HEADER_TRANSFER_ENCODING => 'transfer-encoding';
|
|
|
|
push @EXPORT, qw(S3_HEADER_TRANSFER_ENCODING);
|
|
|
|
use constant S3_HEADER_ETAG => 'etag';
|
|
|
|
push @EXPORT, qw(S3_HEADER_ETAG);
|
|
|
|
|
|
|
|
use constant S3_RESPONSE_TYPE_IO => 'io';
|
|
|
|
push @EXPORT, qw(S3_RESPONSE_TYPE_IO);
|
|
|
|
use constant S3_RESPONSE_TYPE_NONE => 'none';
|
|
|
|
push @EXPORT, qw(S3_RESPONSE_TYPE_NONE);
|
|
|
|
use constant S3_RESPONSE_TYPE_XML => 'xml';
|
|
|
|
push @EXPORT, qw(S3_RESPONSE_TYPE_XML);
|
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
use constant S3_RESPONSE_CODE_SUCCESS => 200;
|
2017-08-09 17:27:09 +02:00
|
|
|
use constant S3_RESPONSE_CODE_ERROR_AUTH => 403;
|
2017-08-08 23:15:01 +02:00
|
|
|
use constant S3_RESPONSE_CODE_ERROR_NOT_FOUND => 404;
|
2018-10-30 22:45:42 +02:00
|
|
|
use constant S3_RESPONSE_CODE_ERROR_RETRY_CLASS => 5;
|
2017-08-08 23:15:01 +02:00
|
|
|
|
2018-10-30 22:45:42 +02:00
|
|
|
use constant S3_RETRY_MAX => 4;
|
2017-08-08 23:15:01 +02:00
|
|
|
|
2017-06-12 16:52:32 +02:00
|
|
|
####################################################################################################################################
|
|
|
|
# new
|
|
|
|
####################################################################################################################################
|
|
|
|
sub new
|
|
|
|
{
|
|
|
|
my $class = shift;
|
|
|
|
|
|
|
|
# Create the class hash
|
|
|
|
my $self = {};
|
|
|
|
bless $self, $class;
|
|
|
|
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
|
|
(
|
|
|
|
my $strOperation,
|
|
|
|
$self->{strBucket},
|
|
|
|
$self->{strEndPoint},
|
|
|
|
$self->{strRegion},
|
|
|
|
$self->{strAccessKeyId},
|
|
|
|
$self->{strSecretAccessKey},
|
2018-05-02 20:06:40 +02:00
|
|
|
$self->{strSecurityToken},
|
2017-06-12 16:52:32 +02:00
|
|
|
$self->{strHost},
|
2017-08-08 23:15:01 +02:00
|
|
|
$self->{iPort},
|
2017-06-12 16:52:32 +02:00
|
|
|
$self->{bVerifySsl},
|
2017-06-23 00:22:49 +02:00
|
|
|
$self->{strCaPath},
|
|
|
|
$self->{strCaFile},
|
2017-06-12 16:52:32 +02:00
|
|
|
$self->{lBufferMax},
|
|
|
|
) =
|
|
|
|
logDebugParam
|
|
|
|
(
|
|
|
|
__PACKAGE__ . '->new', \@_,
|
2017-10-24 18:35:36 +02:00
|
|
|
{name => 'strBucket'},
|
|
|
|
{name => 'strEndPoint'},
|
|
|
|
{name => 'strRegion'},
|
|
|
|
{name => 'strAccessKeyId', redact => true},
|
|
|
|
{name => 'strSecretAccessKey', redact => true},
|
2018-05-02 20:06:40 +02:00
|
|
|
{name => 'strSecurityToken', optional => true, redact => true},
|
2017-10-24 18:35:36 +02:00
|
|
|
{name => 'strHost', optional => true},
|
|
|
|
{name => 'iPort', optional => true},
|
|
|
|
{name => 'bVerifySsl', optional => true, default => true},
|
|
|
|
{name => 'strCaPath', optional => true},
|
|
|
|
{name => 'strCaFile', optional => true},
|
|
|
|
{name => 'lBufferMax', optional => true, default => COMMON_IO_BUFFER_MAX},
|
2017-06-12 16:52:32 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
# If host is not set then it will be bucket + endpoint
|
|
|
|
$self->{strHost} = defined($self->{strHost}) ? $self->{strHost} : "$self->{strBucket}.$self->{strEndPoint}";
|
|
|
|
|
|
|
|
# Return from function and log return values if any
|
|
|
|
return logDebugReturn
|
|
|
|
(
|
|
|
|
$strOperation,
|
|
|
|
{name => 'self', value => $self, trace => true}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
####################################################################################################################################
|
|
|
|
# request - send a request to S3
|
|
|
|
####################################################################################################################################
|
|
|
|
sub request
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
|
|
my
|
|
|
|
(
|
|
|
|
$strOperation,
|
|
|
|
$strVerb,
|
|
|
|
$strUri,
|
|
|
|
$hQuery,
|
|
|
|
$hHeader,
|
|
|
|
$rstrBody,
|
|
|
|
$strResponseType,
|
|
|
|
$bIgnoreMissing,
|
|
|
|
) =
|
|
|
|
logDebugParam
|
|
|
|
(
|
|
|
|
__PACKAGE__ . '->request', \@_,
|
|
|
|
{name => 'strVerb', trace => true},
|
|
|
|
{name => 'strUri', optional => true, default => '/', trace => true},
|
|
|
|
{name => 'hQuery', optional => true, trace => true},
|
|
|
|
{name => 'hHeader', optional => true, trace => true},
|
|
|
|
{name => 'rstrBody', optional => true, trace => true},
|
|
|
|
{name => 'strResponseType', optional => true, default => S3_RESPONSE_TYPE_NONE, trace => true},
|
|
|
|
{name => 'bIgnoreMissing', optional => true, default => false, trace => true},
|
|
|
|
);
|
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
# Server response
|
|
|
|
my $oResponse;
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
# Allow retries on S3 internal failures
|
2017-08-09 17:27:09 +02:00
|
|
|
my $bRetry;
|
2017-08-08 23:15:01 +02:00
|
|
|
my $iRetryTotal = 0;
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
do
|
|
|
|
{
|
|
|
|
# Assume that a retry will not be attempted which is true in most cases
|
|
|
|
$bRetry = false;
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
# Set content length and hash
|
2018-06-11 20:52:26 +02:00
|
|
|
$hHeader->{&S3_HEADER_CONTENT_SHA256} = defined($rstrBody) ? cryptoHashOne('sha256', $$rstrBody) : PAYLOAD_DEFAULT_HASH;
|
2017-08-08 23:15:01 +02:00
|
|
|
$hHeader->{&S3_HEADER_CONTENT_LENGTH} = defined($rstrBody) ? length($$rstrBody) : 0;
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
# Generate authorization header
|
2017-08-09 17:27:09 +02:00
|
|
|
($hHeader, my $strCanonicalRequest, my $strSignedHeaders, my $strStringToSign) = s3AuthorizationHeader(
|
|
|
|
$self->{strRegion}, "$self->{strBucket}.$self->{strEndPoint}", $strVerb, $strUri, httpQuery($hQuery), s3DateTime(),
|
2018-05-02 20:06:40 +02:00
|
|
|
$hHeader, $self->{strAccessKeyId}, $self->{strSecretAccessKey}, $self->{strSecurityToken},
|
|
|
|
$hHeader->{&S3_HEADER_CONTENT_SHA256});
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
# Send the request
|
|
|
|
my $oHttpClient = new pgBackRest::Common::Http::Client(
|
|
|
|
$self->{strHost}, $strVerb,
|
|
|
|
{iPort => $self->{iPort}, strUri => $strUri, hQuery => $hQuery, hRequestHeader => $hHeader,
|
|
|
|
rstrRequestBody => $rstrBody, bVerifySsl => $self->{bVerifySsl}, strCaPath => $self->{strCaPath},
|
2017-09-03 22:48:41 +02:00
|
|
|
strCaFile => $self->{strCaFile}, bResponseBodyPrefetch => $strResponseType eq S3_RESPONSE_TYPE_XML,
|
|
|
|
lBufferMax => $self->{lBufferMax}});
|
2017-08-08 23:15:01 +02:00
|
|
|
|
|
|
|
# Check response code
|
2017-08-09 17:27:09 +02:00
|
|
|
my $iResponseCode = $oHttpClient->responseCode();
|
2017-08-08 23:15:01 +02:00
|
|
|
|
2017-08-09 17:27:09 +02:00
|
|
|
if ($iResponseCode == S3_RESPONSE_CODE_SUCCESS)
|
2017-06-12 16:52:32 +02:00
|
|
|
{
|
2017-08-08 23:15:01 +02:00
|
|
|
# Save the response headers locally
|
|
|
|
$self->{hResponseHeader} = $oHttpClient->responseHeader();
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
# XML response is expected
|
|
|
|
if ($strResponseType eq S3_RESPONSE_TYPE_XML)
|
2017-06-12 16:52:32 +02:00
|
|
|
{
|
2017-08-08 23:15:01 +02:00
|
|
|
my $rtResponseBody = $oHttpClient->responseBody();
|
2017-06-12 16:52:32 +02:00
|
|
|
|
2017-08-08 23:15:01 +02:00
|
|
|
if ($oHttpClient->contentLength() == 0 || !defined($$rtResponseBody))
|
|
|
|
{
|
|
|
|
confess &log(ERROR,
|
|
|
|
"response type '${strResponseType}' was requested but content length is zero or content is missing",
|
|
|
|
ERROR_PROTOCOL);
|
|
|
|
}
|
|
|
|
|
|
|
|
$oResponse = xmlParse($$rtResponseBody);
|
|
|
|
}
|
|
|
|
# An IO object is expected for file responses
|
|
|
|
elsif ($strResponseType eq S3_RESPONSE_TYPE_IO)
|
2017-06-12 16:52:32 +02:00
|
|
|
{
|
2017-08-08 23:15:01 +02:00
|
|
|
$oResponse = $oHttpClient;
|
2017-06-12 16:52:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2017-08-08 23:15:01 +02:00
|
|
|
# If file was not found
|
2017-08-09 17:27:09 +02:00
|
|
|
if ($iResponseCode == S3_RESPONSE_CODE_ERROR_NOT_FOUND)
|
2017-08-08 23:15:01 +02:00
|
|
|
{
|
|
|
|
# If missing files should not be ignored then error
|
|
|
|
if (!$bIgnoreMissing)
|
|
|
|
{
|
|
|
|
confess &log(ERROR, "unable to open '${strUri}': No such file or directory", ERROR_FILE_MISSING);
|
|
|
|
}
|
|
|
|
|
|
|
|
$bRetry = false;
|
|
|
|
}
|
2017-08-09 17:27:09 +02:00
|
|
|
# Else a more serious error
|
2017-08-08 23:15:01 +02:00
|
|
|
else
|
|
|
|
{
|
2018-10-30 22:45:42 +02:00
|
|
|
# Retry for S3 internal or rate-limiting errors (any 5xx error should be retried)
|
|
|
|
if (int($iResponseCode / 100) == S3_RESPONSE_CODE_ERROR_RETRY_CLASS)
|
2017-08-08 23:15:01 +02:00
|
|
|
{
|
2017-08-09 17:27:09 +02:00
|
|
|
# Increment retry total and check if retry should be attempted
|
2017-08-08 23:15:01 +02:00
|
|
|
$iRetryTotal++;
|
|
|
|
$bRetry = $iRetryTotal <= S3_RETRY_MAX;
|
2017-08-09 17:27:09 +02:00
|
|
|
|
|
|
|
# Sleep after first retry just in case data needs to stabilize
|
|
|
|
if ($iRetryTotal > 1)
|
|
|
|
{
|
|
|
|
sleep(5);
|
|
|
|
}
|
2017-08-08 23:15:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# If no retry then throw the error
|
|
|
|
if (!$bRetry)
|
|
|
|
{
|
|
|
|
my $rstrResponseBody = $oHttpClient->responseBody();
|
|
|
|
|
|
|
|
confess &log(ERROR,
|
2018-10-30 22:45:42 +02:00
|
|
|
'S3 request error' . ($iRetryTotal > 0 ? " after " . (S3_RETRY_MAX + 1) . " tries" : '') .
|
2017-08-09 17:27:09 +02:00
|
|
|
" [$iResponseCode] " . $oHttpClient->responseMessage() .
|
2017-08-08 23:15:01 +02:00
|
|
|
"\n*** request header ***\n" . $oHttpClient->requestHeaderText() .
|
2017-08-09 17:27:09 +02:00
|
|
|
($iResponseCode == S3_RESPONSE_CODE_ERROR_AUTH ?
|
|
|
|
"\n*** canonical request ***\n" . $strCanonicalRequest .
|
|
|
|
"\n*** signed headers ***\n" . $strSignedHeaders .
|
|
|
|
"\n*** string to sign ***\n" . $strStringToSign : '') .
|
|
|
|
"\n*** response header ***\n" . $oHttpClient->responseHeaderText() .
|
2017-08-08 23:15:01 +02:00
|
|
|
(defined($$rstrResponseBody) ? "\n*** response body ***\n${$rstrResponseBody}" : ''),
|
|
|
|
ERROR_PROTOCOL);
|
|
|
|
}
|
|
|
|
}
|
2017-06-12 16:52:32 +02:00
|
|
|
}
|
|
|
|
}
|
2017-08-08 23:15:01 +02:00
|
|
|
while ($bRetry);
|
2017-06-12 16:52:32 +02:00
|
|
|
|
|
|
|
# Return from function and log return values if any
|
|
|
|
return logDebugReturn
|
|
|
|
(
|
|
|
|
$strOperation,
|
|
|
|
{name => 'oResponse', value => $oResponse, trace => true, ref => true}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|