Skip to content

Instantly share code, notes, and snippets.

@s1037989
Last active September 28, 2025 19:09
Show Gist options
  • Select an option

  • Save s1037989/479087c752d7461d3499c12971636678 to your computer and use it in GitHub Desktop.

Select an option

Save s1037989/479087c752d7461d3499c12971636678 to your computer and use it in GitHub Desktop.
Use Mojo::UserAgent::Role::AWSSignature4 to interface with AWS REST API, such as controlling EC2 or working uploading and downloading S3 content. Works with S3-compatible APIs, of course, such as Digital Ocean Spaces and Wasabi.
use Mojo::Base -strict;
use Test::More;
plan skip_all => 'set AWS_ACCESS_KEY and AWS_SECRET_KEY to enable this test'
unless $ENV{AWS_ACCESS_KEY} && $ENV{AWS_SECRET_KEY};
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->with_roles('+AWSSignature4')->new;
my $tx = $ua->build_tx(GET => 'https://ec2.amazonaws.com?Action=DescribeVolumes&Version=2016-11-15' => awssig4 => {
debug => 1,
expires => undef,
region => 'us-east-2',
service => 'ec2',
});
like $tx->req->headers->authorization, qr($ENV{AWS_ACCESS_KEY}), 'authorization contains access_key';
# diag explain $tx->req->headers->to_hash;
my $res = $ua->start($tx)->res;
ok $res->dom->find('requestId'), 'has a requestId';
# diag $res->body;
done_testing;
package Mojo::UserAgent::Role::AWSSignature4;
use Mojo::Base -role, -signatures;
use Digest::SHA;
use Mojo::Collection;
use Time::Piece;
has access_key => sub { $ENV{AWS_ACCESS_KEY} or die 'missing "access_key"' };
has aws_algorithm => 'AWS4-HMAC-SHA256';
has content => undef;
has debug => 0;
has expires => 86_400;
has region => 'us-east-1';
has secret_key => sub { $ENV{AWS_SECRET_KEY} or die 'missing "secret_key"' };
has service => sub { die 'missing "service"' };
has session_token => sub { $ENV{AWS_SESSION_TOKEN} || undef };
has unsigned_payload => 0;
has _tx => sub { die };
around build_tx => sub ($orig, $self, @args) {
$self->transactor->add_generator(awssig4 => sub {
my ($transactor, $tx, $config) = @_;
my $aws = $self->new({%$config, _tx => $tx});
$tx->req->content->asset(Mojo::Asset::File->new(path => $aws->content)) if $aws->content;
$tx->req->headers->host($tx->req->url->host || 'localhost');
$tx->req->headers->header('X-Amz-Date' => $aws->date_timestamp);
$tx->req->headers->header('X-Amz-Content-Sha256' => $aws->hashed_payload);
$tx->req->headers->header('X-Amz-Expires' => $aws->expires) if $aws->expires;
$tx->req->headers->authorization($aws->authorization);
});
$orig->($self, @args);
};
sub authorization ($self) {
sprintf
'%s Credential=%s/%s, SignedHeaders=%s, Signature=%s',
$self->aws_algorithm,
$self->access_key,
$self->credential_scope,
$self->signed_header_list,
$self->signature
}
sub canonical_headers ($self) {
join '', map { lc($_) . ":" . $self->_tx->req->headers->to_hash->{$_} . "\n" } @{$self->header_list};
}
sub canonical_qstring { shift->_tx->req->url->query->to_string }
sub canonical_request ($self) {
Mojo::Collection->new(
$self->_tx->req->method,
$self->_tx->req->url->path->to_abs_string,
$self->canonical_qstring,
$self->canonical_headers,
$self->signed_header_list,
$self->hashed_payload
)->tap(sub{ warn $_->map(sub { "CR:$_" })->join("\n") if $self->debug })->join("\n");
}
sub credential_scope ($self) {
join '/', $self->date, $self->region, $self->service, 'aws4_request';
}
sub date { shift->time->ymd('') }
sub date_timestamp { $_[0]->time->ymd('').'T'.$_[0]->time->hms('').'Z' }
sub hashed_payload ($self) {
$self->unsigned_payload ? 'UNSIGNED-PAYLOAD' : Digest::SHA::sha256_hex($self->_tx->req->body);
}
sub header_list { [sort keys %{shift->_tx->req->headers->to_hash}] }
sub signature ($self) {
Digest::SHA::hmac_sha256_hex($self->string_to_sign, $self->signing_key);
}
sub signed_header_list { join ';', map { lc($_) } @{shift->header_list} }
sub signed_qstring ($self) {
$self->_tx->req->url->query(['X-Amz-Signature' => $self->signature]);
}
sub signing_key ($self) {
my $kSecret = "AWS4" . $self->secret_key;
my $kDate = Digest::SHA::hmac_sha256($self->date, $kSecret);
my $kRegion = Digest::SHA::hmac_sha256($self->region, $kDate);
my $kService = Digest::SHA::hmac_sha256($self->service, $kRegion);
Digest::SHA::hmac_sha256("aws4_request", $kService);
}
sub string_to_sign ($self) {
Mojo::Collection->new(
$self->aws_algorithm,
$self->date_timestamp,
$self->credential_scope,
Digest::SHA::sha256_hex($self->canonical_request)
)->tap(sub{ warn $_->map(sub { "STS:$_" })->join("\n") if $self->debug })->join("\n");
}
sub time { gmtime }
1;
use Mojo::Base -strict, -signatures;
use Test::More;
plan skip_all => 'set AWS_ACCESS_KEY and AWS_SECRET_KEY to enable this test'
unless $ENV{AWS_ACCESS_KEY} && $ENV{AWS_SECRET_KEY};
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->with_roles('+AWSSignature4')->new;
$ua->transactor->add_generator(stream => sub ($transactor, $tx, $path) {
$tx->req->content->asset(Mojo::Asset::File->new(path => $path));
});
subtest 'Describe Volumes : S3 API' => sub {
my $tx = $ua->build_tx(GET => 'https://ec2.amazonaws.com?Action=DescribeVolumes&Version=2016-11-15' => awssig4 => {
debug => 1,
expires => undef,
service => 's3',
});
like $tx->req->headers->authorization, qr($ENV{AWS_ACCESS_KEY}), 'authorization contains access_key';
# diag explain $tx->req->headers->to_hash;
my $res = $ua->start($tx)->res;
ok $res->dom->find('requestId'), 'has a requestId';
# diag $res->body;
};
subtest 'Upload Object : S3 API' => sub {
my $tx = $ua->build_tx(PUT => sprintf('ec2.amazonaws.com/%s', __FILE__) => awssig4 => {
debug => 1,
expires => undef,
service => 's3',
} => stream => __FILE__); # instead of using "stream" generator, could also do "path(__FILE__)->slurp"
like $tx->req->headers->authorization, qr($ENV{AWS_ACCESS_KEY}), 'authorization contains access_key';
# diag explain $tx->req->headers->to_hash;
my $res = $ua->start($tx)->res;
ok $res->dom->find('requestId'), 'has a requestId';
# diag $res->body;
};
done_testing;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment