Amazon-S3-Thin

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

Revision history for Perl extension Amazon-S3-Thin

0.32 2022-08-24T00:32:45Z

   - [core] Fetch credentials from ECS task role (#43 @masawada++)

0.31 2022-08-11T17:20:41Z

   - [bugfix] Fix a behavior of creating a session token with IMDSv2 (#41 @masawada++)

0.30 2020-08-28T11:53:24Z

   - [core] support extra credential providers

0.29 2020-05-08T11:20:28Z

Changes  view on Meta::CPAN

   - [Signature v4] Make it work with signature v4. (use path style addressing for v4) (@showaltb)
   - [Signature v4] Add `region` option. It is required by default (break B.C.)  (@showaltb)
   - [Signature v4] Add `signature_version` option.  (@showaltb)
   - [Core] Remove `host` option (break B.C.)
   - [S3 api]Add API calls of `put_bucket` and `delete_bucket`
   - [Core] Suppresses uri encoding of the bucket key.  (@showaltb)
   - [Environment] make it work with Carton on travis
   - [Dev] Add `debug` option

   - Internal design
   - Add Credentials class to contain aws credentials
   - Add Resource class to express URI
   
0.16 2015-05-08T04:51:36Z
   - get_object() takes optinal headers

0.15 2015-04-26T06:44:48Z
   - add garu to contributors

0.14 2015-04-25T19:36:32Z

MANIFEST  view on Meta::CPAN

eg/s3
lib/Amazon/S3/Thin.pm
lib/Amazon/S3/Thin/Credentials.pm
lib/Amazon/S3/Thin/Resource.pm
lib/Amazon/S3/Thin/Signer/V2.pm
lib/Amazon/S3/Thin/Signer/V4.pm
minil.toml
t/00_compile.t
t/01_accessors.t
t/01_new.t
t/02_credentials_ecs_container.t
t/02_credentials_metadata.t
t/02_signer_v2.t
t/02_signer_v4.t
t/03_request.t
t/04_request_v2.t
t/05_presigned_post.t
t/06_request_virtual_host.t
t/07_copy_200_error.t
xt/90_functional.t
xt/91_functional.t
xt/92_presigned_post.t

README.md  view on Meta::CPAN

[![Actions Status](https://github.com/DQNEO/Amazon-S3-Thin/workflows/test/badge.svg)](https://github.com/DQNEO/Amazon-S3-Thin/actions)
# NAME

Amazon::S3::Thin - A thin, lightweight, low-level Amazon S3 client

# SYNOPSIS

    use Amazon::S3::Thin;

    # Pass in explicit credentials
    my $s3client = Amazon::S3::Thin->new({
          aws_access_key_id     => $aws_access_key_id,
          aws_secret_access_key => $aws_secret_access_key,
          aws_session_token     => $aws_session_token, # optional
          region                => $region, # e.g. 'ap-northeast-1'
        });

    # Get credentials from environment
    my $s3client = Amazon::S3::Thin->new({region => $region, credential_provider => 'env'});

    # Get credentials from instance metadata
    my $s3client = Amazon::S3::Thin->new({
        region              => $region,
        credential_provider => 'metadata',
        version             => 2,         # optional (default 2)
        role                => 'my-role', # optional
      });

    # Get credentials from ECS task role
    my $s3client = Amazon::S3::Thin->new({
        region              => $region,
        credential_provider => 'ecs_container',
      });

    my $bucket = "mybucket";
    my $key = "dir/file.txt";
    my $response;

    $response = $s3client->put_bucket($bucket);

README.md  view on Meta::CPAN

# CONSTRUCTOR

## new( \\%params )

**Receives:** hashref with options.

**Returns:** Amazon::S3::Thin object

It can receive the following arguments:

- `credential_provider` (**default: credentials**) - specify where to source credentials from. Options are:
    - `credentials` - existing behaviour, pass in credentials via `aws_access_key_id` and `aws_secret_access_key`
    - `env` - fetch credentials from environment variables
    - `metadata` - fetch credentials from EC2 instance metadata service
    - `ecs_container` - fetch credentials from ECS task role
- `region` - (**REQUIRED**) region of your buckets you access- (currently used only when signature version is 4)
- `aws_access_key_id` (**REQUIRED \[provider: credentials\]**) - an access key id
of your credentials.
- `aws_secret_access_key` (**REQUIRED \[provider: credentials\]**) - an secret access key
 of your credentials.
- `version` (**OPTIONAL \[provider: metadata\]**) - version of metadata service to use, either 1 or 2.
[read more](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)
- `role` (**OPTIONAL \[provider: metadata\]**) - IAM instance role to use, otherwise the first is selected
- `secure` - whether to use https or not. Default is 0 (http).
- `ua` - a user agent object, compatible with LWP::UserAgent.
Default is an instance of [LWP::UserAgent](https://metacpan.org/pod/LWP%3A%3AUserAgent).
- `signature_version` - AWS signature version to use. Supported values
are 2 and 4. Default is 4.
- `debug` - debug option. Default is 0 (false). 
If set 1, contents of HTTP request and response are shown on stderr

eg/s3  view on Meta::CPAN

    if ($version) {
        printf "s3 %s\n", $VERSION;
        exit 0;
    }
    if ($help) {
        $self->help();
        exit 0;
    }

    # https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
    my $cred_file = $ENV{HOME} . "/.aws/credentials";
    my $config_file = $ENV{HOME} . "/.aws/config";

    if (-f $config_file and !defined $region) {
        my $config = Config::Tiny->read($config_file)->{$profile};
        $region = $config->{region};
    }

    if (-f $cred_file) {
        my $crd = Config::Tiny->read($cred_file)->{$profile};

lib/Amazon/S3/Thin.pm  view on Meta::CPAN

my $METADATA_PREFIX      = 'x-amz-meta-';
my $MAIN_HOST = 's3.amazonaws.com';

sub new {
    my $class = shift;
    my $self  = shift;

    # If we have an explicitly-configured credential provider then use that here, otherwise
    # existing behaviour will be followed
    if ($self->{credential_provider} and $self->{credential_provider} eq 'env') {
        $self->{credentials} = Amazon::S3::Thin::Credentials->from_env;
    }
    elsif ($self->{credential_provider} and $self->{credential_provider} eq 'metadata') {
        $self->{credentials} = Amazon::S3::Thin::Credentials->from_metadata($self);
    }
    elsif ($self->{credential_provider} and $self->{credential_provider} eq 'ecs_container') {
        $self->{credentials} = Amazon::S3::Thin::Credentials->from_ecs_container($self);
    }
    else {
        # check existence of credentials
        croak "No aws_access_key_id"     unless $self->{aws_access_key_id};
        croak "No aws_secret_access_key" unless $self->{aws_secret_access_key};

        # wrap credentials
        $self->{credentials} = Amazon::S3::Thin::Credentials->new(
            $self->{aws_access_key_id},
            $self->{aws_secret_access_key},
            $self->{aws_session_token},
        );
        delete $self->{aws_access_key_id};
        delete $self->{aws_secret_access_key};
        delete $self->{aws_session_token};
    }
    delete $self->{credential_provider};

lib/Amazon/S3/Thin.pm  view on Meta::CPAN

    return $self;
}

sub _load_signer {
  my $self = shift;
  my $version = shift;
  my $signer_class = "Amazon::S3::Thin::Signer::V$version";
  eval "require $signer_class" or die $@;

  if ($version == 2) {
      return $signer_class->new($self->{credentials}, $MAIN_HOST);
  } elsif ($version == 4) {
      return $signer_class->new($self->{credentials}, $self->{region});
  }
}


sub _default_ua {
    my $self = shift;

    my $ua = LWP::UserAgent->new(
        keep_alive            => 10,
        requests_redirectable => [qw(GET HEAD DELETE PUT)],

lib/Amazon/S3/Thin.pm  view on Meta::CPAN

__END__

=head1 NAME

Amazon::S3::Thin - A thin, lightweight, low-level Amazon S3 client

=head1 SYNOPSIS

  use Amazon::S3::Thin;

  # Pass in explicit credentials
  my $s3client = Amazon::S3::Thin->new({
        aws_access_key_id     => $aws_access_key_id,
        aws_secret_access_key => $aws_secret_access_key,
        aws_session_token     => $aws_session_token, # optional
        region                => $region, # e.g. 'ap-northeast-1'
      });

  # Get credentials from environment
  my $s3client = Amazon::S3::Thin->new({region => $region, credential_provider => 'env'});

  # Get credentials from instance metadata
  my $s3client = Amazon::S3::Thin->new({
      region              => $region,
      credential_provider => 'metadata',
      version             => 2,         # optional (default 2)
      role                => 'my-role', # optional
    });

  # Get credentials from ECS task role
  my $s3client = Amazon::S3::Thin->new({
      region              => $region,
      credential_provider => 'ecs_container',
    });

  my $bucket = "mybucket";
  my $key = "dir/file.txt";
  my $response;

  $response = $s3client->put_bucket($bucket);

lib/Amazon/S3/Thin.pm  view on Meta::CPAN

=head2 new( \%params )

B<Receives:> hashref with options.

B<Returns:> Amazon::S3::Thin object

It can receive the following arguments:

=over 4

=item * C<credential_provider> (B<default: credentials>) - specify where to source credentials from. Options are:

=over 2

=item * C<credentials> - existing behaviour, pass in credentials via C<aws_access_key_id> and C<aws_secret_access_key>

=item * C<env> - fetch credentials from environment variables

=item * C<metadata> - fetch credentials from EC2 instance metadata service

=item * C<ecs_container> - fetch credentials from ECS task role

=back

=item * C<region> - (B<REQUIRED>) region of your buckets you access- (currently used only when signature version is 4)

=item * C<aws_access_key_id> (B<REQUIRED [provider: credentials]>) - an access key id
of your credentials.

=item * C<aws_secret_access_key> (B<REQUIRED [provider: credentials]>) - an secret access key
 of your credentials.

=item * C<version> (B<OPTIONAL [provider: metadata]>) - version of metadata service to use, either 1 or 2.
L<read more|https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html>

=item * C<role> (B<OPTIONAL [provider: metadata]>) - IAM instance role to use, otherwise the first is selected

=item * C<secure> - whether to use https or not. Default is 0 (http).

=item * C<ua> - a user agent object, compatible with LWP::UserAgent.
Default is an instance of L<LWP::UserAgent>.

lib/Amazon/S3/Thin/Credentials.pm  view on Meta::CPAN

package Amazon::S3::Thin::Credentials;

=head1 NAME

Amazon::S3::Thin::Credentials - AWS credentials data container

=head1 SYNOPSIS

    my $credentials = Amazon::S3::Thin::Credentials->new(
        $aws_access_key_id, $aws_secret_access_key,
        # optional:
        $aws_session_token
    );
    
    my $key = $credentials->access_key_id();
    my $secret = $credentials->secret_access_key();
    my $session_token = $credentials->session_token();

1;

=head1 DESCRIPTION

This module contains AWS credentials and provide getters to the data.

    # Load from arguments
    my $creds = Amazon::S3::Thin::Credentials->new($access_key, $secret_key, $session_token);

    # Load from environment
    my $creds = Amazon::S3::Thin::Credentials->from_env;

    # Load from instance profile
    my $creds = Amazon::S3::Thin::Credentials->from_metadata(role => 'foo', version => 2);

lib/Amazon/S3/Thin/Credentials.pm  view on Meta::CPAN

    my $self = {
        key => $key,
        secret => $secret,
        session_token => $session_token,
    };
    return bless $self, $class;
}

=head2 from_env()

Instantiate C<Amazon::S3::Thin::Credentials> and attempts to populate the credentials from
current environment.

Croaks if either AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY are not set but supports the
optional AWS_SESSION_TOKEN variable.

    my $creds = Amazon::S3::Thin::Credentials->from_env;

=cut

sub from_env {

lib/Amazon/S3/Thin/Credentials.pm  view on Meta::CPAN

    my $self = {
        key => $ENV{AWS_ACCESS_KEY_ID},
        secret => $ENV{AWS_SECRET_ACCESS_KEY},
        session_token => $ENV{AWS_SESSION_TOKEN}
    };
    return bless $self, $class;
}

=head2 from_metadata()

Instantiate C<Amazon::S3::Thin::Credentials> and attempts to populate the credentials from
the L<EC2 metadata service|https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html>. An instance can have multiple IAM
roles applied so you may optionally specify a role, otherwise the first one will be used.

In November 2019 AWS released L<version 2|https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/> of the instance metadata service which
is more secure against Server Side Request Forgery attacks. Using v2 is highly recommended thus
it is the default here.

    my $creds = Amazon::S3::Thin::Credentials->from_metadata(
        role => 'foo',      # The name of the IAM role on the instance
        version => 2        # Metadata service version - either 1 or 2

lib/Amazon/S3/Thin/Credentials.pm  view on Meta::CPAN


        $ua->default_header('X-aws-ec2-metadata-token' => $res->decoded_content);
    }

    return _instance_metadata($ua, $args->{role});
}

sub _instance_metadata {
    my ($ua, $role) = @_;

    my $res = $ua->get('http://169.254.169.254/latest/meta-data/iam/security-credentials');
    croak 'Error querying metadata service for roles: ' . $res->decoded_content unless $res->is_success;

    my @roles = split /\n/, $res->decoded_content;
    return unless @roles > 0;

    my $target_role = (defined $role and grep { $role eq $_ } @roles)
        ? $role
        : $roles[0];

    my $cred = $ua->get('http://169.254.169.254/latest/meta-data/iam/security-credentials/' . $target_role);
    croak 'Error querying metadata service for credentials: ' . $cred->decoded_content unless $cred->is_success;

    my $obj = eval { $JSON->decode($cred->decoded_content) };
    croak "Invalid data returned from metadata service: $@" if $@;

    return __PACKAGE__->new($obj->{AccessKeyId}, $obj->{SecretAccessKey}, $obj->{Token});
}

=head2 from_ecs_container()

Instantiate C<Amazon::S3::Thin::Credentials> and attempts to populate the credentials from
the L<ECS task role|https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>.

    my $creds = Amazon::S3::Thin::Credentials->from_ecs_container;

=cut

sub from_ecs_container {
  my ($class, $args) = @_;

  my $ua = $args->{ua} // LWP::UserAgent->new;

  my $relative_uri = $ENV{AWS_CONTAINER_CREDENTIALS_RELATIVE_URI};
  croak 'The environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is not set' unless defined $relative_uri;

  my $cred = $ua->get('http://169.254.170.2' . $relative_uri);
  croak 'Error retrieving container credentials' unless $cred->is_success;

  my $obj = eval { $JSON->decode($cred->decoded_content) };
  croak "Invalid data returned: $@" if $@;

  return __PACKAGE__->new($obj->{AccessKeyId}, $obj->{SecretAccessKey}, $obj->{Token});
}

=head2 access_key_id()

Returns access_key_id

lib/Amazon/S3/Thin/Signer/V2.pm  view on Meta::CPAN

my $AMAZON_HEADER_PREFIX = 'x-amz-';

# reserved subresources such as acl or torrent
our @ordered_subresources = qw(
        acl delete lifecycle location logging notification partNumber policy
        requestPayment torrent uploadId uploads versionId versioning versions
        website
    );

sub new {
    my ($class, $credentials, $host) = @_;
    if (ref($credentials) ne 'Amazon::S3::Thin::Credentials') {
        croak "credentials object is not given."
    }
    my $self = {
        credentials => $credentials,
        host => $host,
    };
    bless $self, $class;
}

sub sign
{
  my ($self, $request) = @_;
  $request->header(Date => HTTP::Date::time2str(time)) unless $request->header('Date');
  if (defined $self->{credentials}->session_token) {
    $request->header('X-Amz-Security-Token', $self->{credentials}->session_token);
  }
  my $host = $request->uri->host;
  my $bucket = substr($host, 0, length($host) - length($self->{host}) - 1);
  my $path = $bucket . $request->uri->path;
  my $signature = $self->calculate_signature( $request->method, $path, $request->headers );
  $request->header(
    Authorization => sprintf("AWS %s:%s"
      , $self->{credentials}->access_key_id,
      , $signature));
}

# generate a canonical string for the given parameters.  expires is optional and is
# only used by query string authentication.
sub calculate_signature {
    my ($self, $method, $path, $headers, $expires) = @_;

    my $string_to_sign = $self->string_to_sign( $method, $path, $headers, $expires );

    my $hmac = Digest::HMAC_SHA1->new($self->{credentials}->secret_access_key);
    $hmac->add($string_to_sign);
    return MIME::Base64::encode_base64($hmac->digest, '');
}

sub string_to_sign {
    my ($self, $method, $path, $headers, $expires) = @_;

    my %interesting_headers = ();
    while (my ($key, $value) = each %$headers) {
        my $lk = lc $key;

lib/Amazon/S3/Thin/Signer/V4.pm  view on Meta::CPAN


use strict;
use warnings;
use AWS::Signature4;
use Digest::SHA ();
use JSON::PP ();
use MIME::Base64 ();
use POSIX 'strftime';

sub new {
    my ($class, $credentials, $region) = @_;
    my $self = {
        credentials => $credentials,
        region => $region,
    };
    bless $self, $class;
}

=head1 METHODS

=head2 sign($request)

Signs supplied L<HTTP::Request> object, adding required AWS headers.

=cut

sub sign
{
  my ($self, $request) = @_;
  my $signer = $self->signer;
  if (defined $self->{credentials}->session_token) {
    $request->header('X-Amz-Security-Token', $self->{credentials}->session_token);
  }
  my $digest = Digest::SHA::sha256_hex($request->content);
  $request->header('X-Amz-Content-SHA256', $digest);
  $signer->sign($request, $self->{region}, $digest);
  $request;
}

=head2 signer

Returns an L<AWS::Signature4> object for signing requests

=cut

sub signer
{
  my $self = shift;
  AWS::Signature4->new(
    -access_key => $self->{credentials}->access_key_id,
    -secret_key => $self->{credentials}->secret_access_key,
  );
}

# This method is written referencing these botocore's implementations:
# https://github.com/boto/botocore/blob/00c4cadcf0996ef77a3a01b158f15c8fced9909b/botocore/signers.py#L602-L714
# https://github.com/boto/botocore/blob/00c4cadcf0996ef77a3a01b158f15c8fced9909b/botocore/signers.py#L459-L528
# https://github.com/boto/botocore/blob/00c4cadcf0996ef77a3a01b158f15c8fced9909b/botocore/auth.py#L585-L628
sub _generate_presigned_post {
    my ($self, $bucket, $key, $fields, $conditions, $expires_in) = @_;

lib/Amazon/S3/Thin/Signer/V4.pm  view on Meta::CPAN

    }

    push @$fields, 'x-amz-algorithm' => 'AWS4-HMAC-SHA256';
    push @$fields, 'x-amz-credential' => $credential;
    push @$fields, 'x-amz-date' => $datetime;

    push @$conditions, {'x-amz-algorithm' => 'AWS4-HMAC-SHA256'};
    push @$conditions, {'x-amz-credential' => $credential};
    push @$conditions, {'x-amz-date' => $datetime};

    my $session_token = $self->{credentials}->session_token;
    if (defined $session_token) {
        push @$fields, 'x-amz-security-token' => $session_token;
        push @$conditions, {'x-amz-security-token' => $session_token};
    }

    my $policy = $self->_encode_policy({
        expiration => $expiration,
        conditions => $conditions,
    });
    push @$fields, policy => $policy;

t/02_credentials_ecs_container.t  view on Meta::CPAN

use warnings;
use Amazon::S3::Thin::Credentials;
use Test::More;

my $arg = +{
    credential_provider => 'ecs_container',
    region              => 'ap-northeast-1',
};

{
    diag "retrieve credentials from the ECS task role";

    local $ENV{AWS_CONTAINER_CREDENTIALS_RELATIVE_URI} = '/foobar';

    my $ua = MockUA->new;
    my $credentials = Amazon::S3::Thin::Credentials->from_ecs_container(+{ ua => $ua });

    is_deeply $ua->requests, [
        {
            method  => 'GET',
            uri     => 'http://169.254.170.2/foobar',
        },
    ];

    is $credentials->access_key_id, 'DUMMY-ACCESS-KEY';
    is $credentials->secret_access_key, 'DUMMY-SECRET-ACCESS-KEY';
    is $credentials->session_token, 'DUMMY-TOKEN';
}

{
    diag "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is not set";

    my $ua = MockUA->new;

    eval {
        my $credentials = Amazon::S3::Thin::Credentials->from_ecs_container(+{ ua => $ua });
    };

    like $@, qr/The environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is not set/;
}

{
    diag "request failed";

    local $ENV{AWS_CONTAINER_CREDENTIALS_RELATIVE_URI} = '/internal_server_error';

    my $ua = MockUA->new;
    eval {
        my $credentials = Amazon::S3::Thin::Credentials->from_ecs_container(+{ ua => $ua });
    };

    like $@, qr/Error retrieving container credentials/;
}

{
    diag "returned content is not JSON";

    local $ENV{AWS_CONTAINER_CREDENTIALS_RELATIVE_URI} = '/not_json';

    my $ua = MockUA->new;
    eval {
        my $credentials = Amazon::S3::Thin::Credentials->from_ecs_container(+{ ua => $ua });
    };

    like $@, qr/Invalid data returned: /;
}

done_testing;

package MockUA;

sub new {

t/02_credentials_metadata.t  view on Meta::CPAN


my $arg = +{
  credential_provider => 'metadata',
  region              => 'ap-northeast-1',
};

{
  diag "IMDSv1";

  my $ua = MockUA->new;
  my $credentials = Amazon::S3::Thin::Credentials->from_metadata(+{
    %$arg,
    ua      => $ua,
    version => 1,
  });

  is_deeply $ua->requests, [
    {
      method  => 'GET',
      uri     => 'http://169.254.169.254/latest/meta-data/iam/security-credentials',
      headers => {},
    },
    {
      method => 'GET',
      uri     => 'http://169.254.169.254/latest/meta-data/iam/security-credentials/DUMMY-INSTANCE-PROFILE-1',
      headers => {},
    },
  ];

  is $credentials->access_key_id, 'DUMMY-ACCESS-KEY';
  is $credentials->secret_access_key, 'DUMMY-SECRET-ACCESS-KEY';
  is $credentials->session_token, 'DUMMY-TOKEN';
}

{
  diag "IMDSv2";

  my $ua = MockUA->new;
  my $credentials = Amazon::S3::Thin::Credentials->from_metadata(+{
    %$arg,
    ua => $ua,
  });

  is_deeply $ua->requests, [
    {
      method  => 'PUT',
      uri     => 'http://169.254.169.254/latest/api/token',
      headers => { 'X-aws-ec2-metadata-token-ttl-seconds' => 90 },
    },
    {
      method  => 'GET',
      uri     => 'http://169.254.169.254/latest/meta-data/iam/security-credentials',
      headers => { 'X-aws-ec2-metadata-token' => 'DUMMY-METADATA-TOKEN' },
    },
    {
      method => 'GET',
      uri     => 'http://169.254.169.254/latest/meta-data/iam/security-credentials/DUMMY-INSTANCE-PROFILE-1',
      headers => { 'X-aws-ec2-metadata-token' => 'DUMMY-METADATA-TOKEN' },
    },
  ];

  is $credentials->access_key_id, 'DUMMY-ACCESS-KEY';
  is $credentials->secret_access_key, 'DUMMY-SECRET-ACCESS-KEY';
  is $credentials->session_token, 'DUMMY-TOKEN';
}

{
  diag "test when a role name is specified";

  my $ua = MockUA->new;
  my $credentials = Amazon::S3::Thin::Credentials->from_metadata(+{
    %$arg,
    ua      => $ua,
    role    => 'DUMMY-INSTANCE-PROFILE-3',
    version => 1,
  });

  is_deeply $ua->requests, [
    {
      method  => 'GET',
      uri     => 'http://169.254.169.254/latest/meta-data/iam/security-credentials',
      headers => {},
    },
    {
      method => 'GET',
      uri     => 'http://169.254.169.254/latest/meta-data/iam/security-credentials/DUMMY-INSTANCE-PROFILE-3',
      headers => {},
    },
  ];

  is $credentials->access_key_id, 'DUMMY-ACCESS-KEY';
  is $credentials->secret_access_key, 'DUMMY-SECRET-ACCESS-KEY';
  is $credentials->session_token, 'DUMMY-TOKEN';
}

done_testing;

package MockUA;

sub new {
  my $class = shift;
  bless { requests => [], default_headers => {} }, $class;
}

t/02_credentials_metadata.t  view on Meta::CPAN


sub is_success { !!1; }

sub decoded_content {
  my $self = shift;

  my $latest_uri = $self->{request}->{uri};

  if ($latest_uri =~ qr{/latest/api/token$}) {
    return 'DUMMY-METADATA-TOKEN';
  } elsif ($latest_uri =~ qr{/latest/meta-data/iam/security-credentials$}) {
    return <<'TEXT';
DUMMY-INSTANCE-PROFILE-1
DUMMY-INSTANCE-PROFILE-2
DUMMY-INSTANCE-PROFILE-3
TEXT
  } elsif ($latest_uri =~ qr{/latest/meta-data/iam/security-credentials/.+$}) {
    return <<'JSON';
{
  "Code" : "Success",
  "LastUpdated" : "2022-08-01T00:00:00Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "DUMMY-ACCESS-KEY",
  "SecretAccessKey" : "DUMMY-SECRET-ACCESS-KEY",
  "Token" : "DUMMY-TOKEN",
  "Expiration" : "2022-08-01T12:00:00Z"
}

t/02_signer_v2.t  view on Meta::CPAN

# no HTTP communication.
{
    diag "test PUT request";
    my $secret = "secretfoobar";
    my $verb = "PUT";
    my $path = "example/file.txt";

    my $hdr = HTTP::Headers->new;
    $hdr->header("content-length", 15);
    $hdr->header("date", 'Sun, 01 Mar 2015 15:11:25 GMT');
    my $credentials = Amazon::S3::Thin::Credentials->new('', $secret);
    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $sig = $signer->calculate_signature($verb,$path,$hdr);

    is $sig, "/WcvruHFtEoxcEMmdsfLJ6iZClw=";
}

{
    diag "test GET request with single subresource";
    my $secret = "somesecret";
    my $verb = "GET";
    my $path = "example/?delete";
    my $date = 'Sun, 01 Mar 2015 15:11:25 GMT';
    my $string_to_sign = "$verb\n\n\n$date\n/$path";

    my $credentials = Amazon::S3::Thin::Credentials->new('', $secret);
    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("date", $date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);
    is $sig, 'IM6VtFJwF3z+lulFGux8tlU4N8Q=', "get with subresource";
}

{
    diag "test GET request with single subresource";
    my $secret = "somesecret";
    my $verb = "GET";
    my $path = 'example/?delete&versionId=4&invalid&acl&location="foo"';
    my $date = 'Sun, 01 Mar 2015 15:11:25 GMT';
    my $string_to_sign = "$verb\n\n\n$date\n/example/?acl&delete&location&versionId";

    my $credentials = Amazon::S3::Thin::Credentials->new('', $secret);
    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("date", $date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);
    is $sig, 'OztMp7iNgvQVKXZQhXeIBz9UHnU=', "get with many subresources";
}


# test cases as described in
# http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationRequestCanonicalization
my $secret = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
my $credentials = Amazon::S3::Thin::Credentials->new('', $secret);
{
    diag "test Amazon example object GET";

    my $verb = "GET";
    my $date = "Tue, 27 Mar 2007 19:36:42 +0000";
    my $path = "johnsmith/photos/puppy.jpg";
    my $string_to_sign = "$verb\n\n\n$date\n/$path";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);
    is $sig, 'bWq2s1WEIj+Ydj0vQ697zp+IXMU=', "puppy test (GET)";

t/02_signer_v2.t  view on Meta::CPAN

{
    diag "test Amazon example object PUT";

    my $verb           = "PUT";
    my $date           = "Tue, 27 Mar 2007 21:15:45 +0000";
    my $path           = "johnsmith/photos/puppy.jpg";
    my $content_type   = "image/jpeg";
    my $content_length = 94328;
    my $string_to_sign = "$verb\n\n$content_type\n$date\n/$path";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);
    $hdr->header("Content-Type", $content_type);
    $hdr->header("Content-Length", $content_length);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );

t/02_signer_v2.t  view on Meta::CPAN


{
    diag "test Amazon example list";

    my $verb           = "GET";
    my $date           = "Tue, 27 Mar 2007 19:42:41 +0000";
    my $path           = "johnsmith/?prefix=photos&max-keys=50&marker=puppy";
    my $user_agent     = "Mozilla/5.0";
    my $string_to_sign = "$verb\n\n\n$date\n/johnsmith/";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);
    $hdr->header("User-Agent", $user_agent);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);

t/02_signer_v2.t  view on Meta::CPAN

}

{
    diag "test Amazon example fetch";

    my $verb           = "GET";
    my $date           = "Tue, 27 Mar 2007 19:44:46 +0000";
    my $path           = "johnsmith/?acl";
    my $string_to_sign = "$verb\n\n\n$date\n/$path";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);
    is $sig, 'c2WLPFtWHVgbEmeEG93a4cG37dM=', "puppy fetch (GET)";

t/02_signer_v2.t  view on Meta::CPAN

{
    diag "test Amazon example delete";

    my $verb           = "DELETE";
    my $date           = "Tue, 27 Mar 2007 21:20:27 +0000";
    my $path           = "johnsmith/photos/puppy.jpg";
    my $user_agent     = "dotnet";
    my $amz_date       = "Tue, 27 Mar 2007 21:20:26 +0000";
    my $string_to_sign = "$verb\n\n\n$amz_date\n/$path";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);
    $hdr->header("User-Agent", $user_agent);
    $hdr->header("x-amz-date", $amz_date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );

t/02_signer_v2.t  view on Meta::CPAN

    my $content_md5    = "4gJE4saaMU4BqNR0kLY+lw==";
    my @x_amz_meta_reviewed_by = ('joe@johnsmith.net', 'jane@johnsmith.net');
    my $x_amz_meta_filechecksum = '0x02661779';
    my $x_amz_meta_checksum_algorithm = 'crc32';
    my $content_disposition = "attachment; filename=database.dat";
    my $content_encoding = "gzip";
    my $content_length   = 5913339;

    my $string_to_sign = "PUT\n4gJE4saaMU4BqNR0kLY+lw==\napplication/x-download\nTue, 27 Mar 2007 21:06:08 +0000\nx-amz-acl:public-read\nx-amz-meta-checksumalgorithm:crc32\nx-amz-meta-filechecksum:0x02661779\nx-amz-meta-reviewedby:joe\@johnsmith.net,...

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);
    $hdr->header("User-Agent", $user_agent);
    $hdr->header("x-amz-acl", $x_amz_acl);
    $hdr->header("content-type", $content_type);
    $hdr->header("Content-MD5", $content_md5);
    $hdr->header("X-Amz-Meta-ReviewedBy", join(',' => @x_amz_meta_reviewed_by));
    $hdr->header("X-Amz-Meta-FileChecksum", $x_amz_meta_filechecksum);
    $hdr->header("X-Amz-Meta-ChecksumAlgorithm", $x_amz_meta_checksum_algorithm);
    $hdr->header("Content-Disposition", $content_disposition);

t/02_signer_v2.t  view on Meta::CPAN

}

{
    diag "test Amazon example list buckets";

    my $verb           = "GET";
    my $date           = "Wed, 28 Mar 2007 01:29:59 +0000";
    my $path           = "";
    my $string_to_sign = "$verb\n\n\n$date\n/$path";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);
    is $sig, 'qGdzdERIC03wnaRNKh6OqZehG9s=', "puppy list buckets (GET)";
}

{
    diag "test Amazon example unicode keys";

    my $verb           = "GET";
    my $date           = "Wed, 28 Mar 2007 01:49:49 +0000";
    my $path           = "dictionary/fran%C3%A7ais/pr%c3%a9f%c3%a8re";
    my $string_to_sign = "$verb\n\n\n$date\n/$path";

    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials);
    my $hdr = HTTP::Headers->new;
    $hdr->header("Date", $date);

    is(
        $signer->string_to_sign($verb,$path,$hdr),
        $string_to_sign,
        'string to sign'
    );
    my $sig = $signer->calculate_signature($verb, $path, $hdr);
    is $sig, 'DNEZGsoieTZ92F3bUfSPQcbGmlM=', "puppy unicode keys";
}

{
  diag "test sign";

  my $request = HTTP::Request->new(GET => 'https://mybucket.s3.amazonaws.com/myfile.txt');
  $request->header('Date' => 'Wed, 28 Mar 2007 01:49:49 +0000');
  my $credentials = Amazon::S3::Thin::Credentials->new('accesskey', 'secretkey');
  my $signer = Amazon::S3::Thin::Signer::V2->new($credentials, 's3.amazonaws.com');
  $signer->sign($request);
  is_deeply ($request->headers, {
      authorization => 'AWS accesskey:Up4jVMLZzEbhnf+Thj0XJ68JREs=',
      date => 'Wed, 28 Mar 2007 01:49:49 +0000',
    }, 'Request headers');
}

{
    diag "test sign (session token)";

    my $request = HTTP::Request->new(GET => 'https://mybucket.s3.amazonaws.com/myfile.txt');
    $request->header('Date' => 'Wed, 28 Mar 2007 01:49:49 +0000');
    my $credentials = Amazon::S3::Thin::Credentials->new('accesskey', 'secretkey', 'sessiontoken');
    my $signer = Amazon::S3::Thin::Signer::V2->new($credentials, 's3.amazonaws.com');
    $signer->sign($request);
    my $headers = $request->headers;
    delete $headers->{'::std_case'};
    is_deeply ($headers, {
        authorization => 'AWS accesskey:jALzlsXtPsSS7qFbE7l2f7Dpx5Y=',
        date => 'Wed, 28 Mar 2007 01:49:49 +0000',
        'x-amz-security-token' => 'sessiontoken',
    }, 'Request headers');
}

t/02_signer_v4.t  view on Meta::CPAN

use strict;
use warnings;
use Amazon::S3::Thin::Signer::V4;
use Amazon::S3::Thin::Credentials;
use Test::More;
use HTTP::Request;

my $credentials = Amazon::S3::Thin::Credentials->new('accesskey', 'secretkey');

{
  diag "test signer";

  my $signer = Amazon::S3::Thin::Signer::V4->new($credentials);
  my $signer_signer = $signer->signer;
  isa_ok($signer_signer, 'AWS::Signature4', 'signer');
  is_deeply($signer_signer, {
      access_key => 'accesskey',
      secret_key => 'secretkey',
    }, 'signer keys');
}

{
  diag "test sign";

  my $request = HTTP::Request->new(GET => 'https://mybucket.s3.amazonaws.com/myfile.txt');
  $request->header('Date' => 'Wed, 28 Mar 2007 01:49:49 +0000');
  my $signer = Amazon::S3::Thin::Signer::V4->new($credentials);
  $signer->sign($request);
  my $headers = [ sort split /\n/, $request->headers->as_string ];
  is_deeply ($headers, [
      'Authorization: AWS4-HMAC-SHA256 Credential=accesskey/20070328/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=0dea3c9b65eede067ce9e38d48558a63924a6a08a8d21c27cfd7de50e5c78d4b',
      'Date: Wed, 28 Mar 2007 01:49:49 +0000',
      'Host: mybucket.s3.amazonaws.com',
      'X-Amz-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
      'X-Amz-Date: 20070328T014949Z',
    ], 'Request headers');
}

{
    diag "test sign (session token)";

    my $request = HTTP::Request->new(GET => 'https://mybucket.s3.amazonaws.com/myfile.txt');
    $request->header('Date' => 'Wed, 28 Mar 2007 01:49:49 +0000');
    my $credentials = Amazon::S3::Thin::Credentials->new('accesskey', 'secretkey', 'sessiontoken');
    my $signer = Amazon::S3::Thin::Signer::V4->new($credentials);
    $signer->sign($request);
    my $headers = [ sort split /\n/, $request->headers->as_string ];
    is_deeply ($headers, [
        'Authorization: AWS4-HMAC-SHA256 Credential=accesskey/20070328/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=54a046a4241ef546a30aec8d9e9a1e91c2d095be24baaef1797350cd2cfef2fd',
        'Date: Wed, 28 Mar 2007 01:49:49 +0000',
        'Host: mybucket.s3.amazonaws.com',
        'X-Amz-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
        'X-Amz-Date: 20070328T014949Z',
        'X-Amz-Security-Token: sessiontoken',
    ], 'Request headers');

xt/90_functional.t  view on Meta::CPAN


use Amazon::S3::Thin;

if (!$ENV{EXTENDED_TESTING}) {
    plan skip_all => 'Skip functional test because it would call S3 APIs and charge real money. $ENV{EXTENDED_TESTING} is not set.';
}

my $debug = 1;
my $use_https = 1;

my $config_file = $ENV{HOME} . '/.aws/credentials';
my $profile = 's3thin';
my $bucket = $ENV{TEST_S3THIN_BUCKET} || 'dqneo-private-test';
my $region = 'ap-northeast-1';
my $host = "s3.$region.amazonaws.com";

my $crd = Config::Tiny->read($config_file)->{$profile};

my $arg = {
    %$crd,
    region => $region,

xt/91_functional.t  view on Meta::CPAN


use Amazon::S3::Thin;

if (!$ENV{EXTENDED_TESTING}) {
    plan skip_all => 'Skip functional test because it would call S3 APIs and charge real money. $ENV{EXTENDED_TESTING} is not set.';
}

my $debug = 1;
my $use_https = 1;

my $config_file = $ENV{HOME} . '/.aws/credentials';
my $profile = 's3thin';
my $bucket = $ENV{TEST_S3THIN_BUCKET} || 'dqneo-private-test';

my $crd = Config::Tiny->read($config_file)->{$profile};

sub test_with_existing_bucket {
    my $crd = shift;
    my $arg = shift;
    diag('Testing with existing resources.');

xt/92_presigned_post.t  view on Meta::CPAN


use Amazon::S3::Thin;

if (!$ENV{EXTENDED_TESTING}) {
    plan skip_all => 'Skip functional test because it would call S3 APIs and charge real money. $ENV{EXTENDED_TESTING} is not set.';
}

my $debug = 1;
my $use_https = 1;

my $config_file = $ENV{HOME} . '/.aws/credentials';
my $profile = 's3thin';
my $bucket = $ENV{TEST_S3THIN_BUCKET} || 'dqneo-private-test';
my $filename = 'xt/upload.txt';
my $content = do {
    open my $fh, '<', $filename or die $!;
    local $/; <$fh>;
};

my $crd = Config::Tiny->read($config_file)->{$profile};

xt/94_virtual_host.t  view on Meta::CPAN


use Amazon::S3::Thin;

if (!$ENV{EXTENDED_TESTING}) {
    plan skip_all => 'Skip functional test because it would call S3 APIs and charge real money. $ENV{EXTENDED_TESTING} is not set.';
}

my $debug = 1;
my $use_https = 1;

my $config_file = $ENV{HOME} . '/.aws/credentials';
my $profile = 's3thin';
my $bucket = $ENV{TEST_S3THIN_BUCKET} || 'dqneo-private-test';
my $region = 'ap-northeast-1';
my $host = "$bucket.s3.amazonaws.com";

my $crd = Config::Tiny->read($config_file)->{$profile};

my $arg = {
    %$crd,
    region       => $region,

xt/95_delete_multiple_objects.t  view on Meta::CPAN


use Amazon::S3::Thin;

if (!$ENV{EXTENDED_TESTING}) {
    plan skip_all => 'Skip functional test because it would call S3 APIs and charge real money. $ENV{EXTENDED_TESTING} is not set.';
}

my $debug = 1;
my $use_https = 1;

my $config_file = $ENV{HOME} . '/.aws/credentials';
my $profile = 's3thin';
my $bucket = $ENV{TEST_S3THIN_BUCKET} || 'dqneo-private-test';
my $region = 'ap-northeast-1';
my $host = "s3.$region.amazonaws.com";

my $crd = Config::Tiny->read($config_file)->{$profile};

my $arg = {
    %$crd,
    region => $region,



( run in 0.364 second using v1.01-cache-2.11-cpan-4d50c553e7e )