Group
Extension

Amazon-PAApi5-Signature/lib/Amazon/PAApi5/Signature.pm

package Amazon::PAApi5::Signature;
use strict;
use warnings;
use Carp qw/croak/;
use POSIX qw/strftime/;
use Digest::SHA qw/sha256_hex hmac_sha256 hmac_sha256_hex/;
use Class::Accessor::Lite (
    rw  => [qw/
        access_key
        secret_key
        payload
        resource_path
        operation
        host
        region
        aws_headers
        str_signed_header
    /],
    ro  => [qw/
        service
        http_method
        hmac_algorithm
        aws4_request
        x_amz_date
        current_date
    /],
);

our $VERSION = '0.05';

sub new {
    my $class      = shift;
    my $access_key = shift or croak 'access_key is required';
    my $secret_key = shift or croak 'secret_key is required';
    my $payload    = shift or croak 'payload is required';
    my $opt        = shift || {};

    my $operation     = $opt->{operation} || 'SearchItems';
    my $resource_path = $opt->{resource_path} ? $opt->{resource_path} : '/paapi5/' . lc($operation);

    return bless {
        access_key     => $access_key,
        secret_key     => $secret_key,
        payload        => $payload,
        resource_path  => $resource_path,
        operation      => $operation,
        host           => $opt->{host}           || 'webservices.amazon.com',
        region         => $opt->{region}         || 'us-east-1',
        service        => $opt->{service}        || 'ProductAdvertisingAPI',
        http_method    => $opt->{http_method}    || 'POST',
        hmac_algorithm => $opt->{hmac_algorithm} || 'AWS4-HMAC-SHA256',
        aws4_request   => $opt->{aws4_request}   || 'aws4_request',
        x_amz_date     => $class->_get_time_stamp,
        current_date   => $class->_get_date,
        aws_headers    => {},
        str_signed_header => '',
    }, $class;
}

sub req_url {
    my ($self) = @_;

    return sprintf("https://%s%s", $self->host, $self->resource_path);
}

sub _prepare_canonical_url {
    my ($self) = @_;

    my $canonical_url = $self->http_method . "\n";

    $canonical_url .= $self->resource_path . "\n\n";

    my $signed_headers = '';
    for my $key (grep { $_ !~ m!content-type! } sort keys %{$self->aws_headers}) {
        $signed_headers .=  lc($key) . ';';
        $canonical_url .= lc($key) . ':' . $self->aws_headers->{$key} . "\n";
    }

    $canonical_url .= "\n";

    $self->str_signed_header(substr($signed_headers, 0, -1)); # remove ';'
    $canonical_url .= $self->str_signed_header . "\n";

    $canonical_url .= sha256_hex($self->payload);

    return $canonical_url;
}

sub _prepare_string_to_sign {
    my ($self, $canonical_url) = @_;

    return join("\n",
        $self->hmac_algorithm,
        $self->x_amz_date,
        join('/', $self->current_date, $self->region, $self->service, $self->aws4_request),
        sha256_hex($canonical_url),
    );
}

sub _calculate_signature {
    my ($self, $string_to_sign) = @_;

    my $signature_key = $self->_get_signature_key;

    return lc(hmac_sha256_hex($string_to_sign, $signature_key));
}

sub _build_authorization_string {
    my ($self, $signature) = @_;

    return $self->hmac_algorithm . ' '
        . 'Credential=' . join('/', $self->access_key, $self->_get_date, $self->region, $self->service, $self->aws4_request)
        . ',SignedHeaders=' . $self->str_signed_header
        . ',Signature=' . $signature
    ;
}

sub headers {
    my ($self) = @_;

    my $aws_headers = $self->aws_headers;

    $aws_headers->{'content-encoding'} = 'amz-1.0';
    $aws_headers->{'content-type'}     = 'application/json; charset=UTF-8';
    $aws_headers->{'host'}             = $self->host;
    $aws_headers->{'x-amz-date'}       = $self->x_amz_date;
    $aws_headers->{'x-amz-target'}     = $self->_build_amz_target;

    my $canonical_url = $self->_prepare_canonical_url;

    my $string_to_sign = $self->_prepare_string_to_sign($canonical_url);

    my $signature = $self->_calculate_signature($string_to_sign);

    $aws_headers->{Authorization} = $self->_build_authorization_string($signature);

    $self->aws_headers($aws_headers);

    return %{$aws_headers};
}

sub headers_as_arrayref {
    return [shift->headers];
}

sub headers_as_hashref {
    return {shift->headers};
}

sub _get_signature_key {
    my ($self) = @_;

    my $k_date    = hmac_sha256($self->current_date, 'AWS4' . $self->secret_key);
    my $k_region  = hmac_sha256($self->region, $k_date);
    my $k_service = hmac_sha256($self->service, $k_region);
    my $k_signing = hmac_sha256($self->aws4_request, $k_service);

    return $k_signing;
}

sub _build_amz_target {
    return 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.' . shift->operation;
}

sub _get_time_stamp {
    return strftime("%Y%m%dT%H%M%SZ", gmtime()); # 20191128T235650Z
}

sub _get_date {
    return strftime("%Y%m%d", gmtime()); # 20191128
}

sub to_request {
    my ($self) = @_;

    return {
        method  => $self->http_method,
        uri     => $self->req_url,
        headers => $self->headers_as_hashref,
        content => $self->payload,
    };
}

1;

__END__

=encoding UTF-8

=head1 NAME

Amazon::PAApi5::Signature - Amazon Product Advertising API(PA-API) 5.0 Helper


=head1 SYNOPSIS

This code is an example of US region.

    use Amazon::PAApi5::Payload;
    use Amazon::PAApi5::Signature;
    use HTTP::Request::Common;
    use LWP::UserAgent;
    use Data::Dumper;

    my $payload = Amazon::PAApi5::Payload->new(
        'PARTNER_TAG'
    );

    my $sig = Amazon::PAApi5::Signature->new(
        'ACCESS_KEY',
        'SECRET_KEY',
        $payload->to_json({
            Keywords    => 'Perl',
            SearchIndex => 'All',
            ItemCount   => 2,
            Resources   => [qw/
                ItemInfo.Title
            /],
        }),
    );

    my $ua = LWP::UserAgent->new;

    my $req = POST $sig->req_url, $sig->headers, Content => $sig->payload;
    my $res = $ua->request($req);

    warn Dumper($res->status_line, $res->content);

NOTE that Product Advertising API 5.0 has usage limit. Please confirm L<https://webservices.amazon.com/paapi5/documentation/troubleshooting/api-rates.html> or a page for your region.

See B<example/> directory of this module for more examples.

L<https://github.com/bayashi/Amazon-PAApi5-Signature/tree/main/example>


=head1 DESCRIPTION

Amazon::PAApi5::Signature generates a request headers and request body for Amazon Product Advertising API(PA-API) 5.0

L<https://webservices.amazon.com/paapi5/documentation/quick-start.html>


=head1 METHODS

=head2 new($access_key, $secret_key, $request_payload, $options)

Constructor

=head2 req_url

Get request URL string

=head2 headers

Get signed HTTP headers as hash

=head3 headers_as_arrayref

=head3 headers_as_hashref

=head2 to_request

Get a hash for HTTP request


=head1 REPOSITORY

=begin html

<a href="https://github.com/bayashi/Amazon-PAApi5-Signature/blob/main/LICENSE"><img src="https://img.shields.io/badge/LICENSE-Artistic%202.0-GREEN.png"></a> <a href="https://github.com/bayashi/Amazon-PAApi5-Signature/actions"><img src="https://github.com/bayashi/Amazon-PAApi5-Signature/workflows/main/badge.svg?_t=1691809967"/></a> <a href="https://coveralls.io/r/bayashi/Amazon-PAApi5-Signature"><img src="https://coveralls.io/repos/bayashi/Amazon-PAApi5-Signature/badge.png?_t=1691809967&branch=main"/></a>

=end html

Amazon::PAApi5::Signature is hosted on github: L<http://github.com/bayashi/Amazon-PAApi5-Signature>

I appreciate any feedback :D


=head1 AUTHOR

Dai Okabayashi E<lt>bayashi@cpan.orgE<gt>


=head1 LICENSE

C<Amazon::PAApi5::Signature> is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0. (Note that, unlike the Artistic License 1.0, version 2.0 is GPL compatible by itself, hence there is no benefit to having an Artistic 2.0 / GPL disjunction.) See the file LICENSE for details.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.