Move some methods around to have them together by call order logic. Also update current test methods with composer based .env reader.
530 lines
18 KiB
PHP
530 lines
18 KiB
PHP
<?php
|
|
|
|
namespace gullevek\AmazonIncentives\AWS;
|
|
|
|
use gullevek\AmazonIncentives\Client\Client;
|
|
use gullevek\AmazonIncentives\Config\Config;
|
|
use gullevek\AmazonIncentives\Exceptions\AmazonErrors;
|
|
use gullevek\AmazonIncentives\Debug\AmazonDebug;
|
|
use gullevek\AmazonIncentives\Response\CancelResponse;
|
|
use gullevek\AmazonIncentives\Response\CreateBalanceResponse;
|
|
use gullevek\AmazonIncentives\Response\CreateResponse;
|
|
|
|
class AWS
|
|
{
|
|
/** @var string What AWS Service to use: Gift Card on Demand (GCOD) */
|
|
public const SERVICE_NAME = 'AGCODService';
|
|
/** @var string */
|
|
public const ACCEPT_HEADER = 'accept';
|
|
/** @var string content-type */
|
|
public const CONTENT_HEADER = 'content-type';
|
|
/** @var string */
|
|
public const HOST_HEADER = 'host';
|
|
/** @var string */
|
|
public const X_AMZ_DATE_HEADER = 'x-amz-date';
|
|
/** @var string */
|
|
public const X_AMZ_TARGET_HEADER = 'x-amz-target';
|
|
/** @var string */
|
|
public const AUTHORIZATION_HEADER = 'Authorization';
|
|
/** @var string type of encryption type */
|
|
public const AWS_SHA256_ALGORITHM = 'AWS4-HMAC-SHA256';
|
|
/** @var string key type to use */
|
|
public const KEY_QUALIFIER = 'AWS4';
|
|
/** @var string */
|
|
public const TERMINATION_STRING = 'aws4_request';
|
|
/** @var string Service to use: Create Gift Card */
|
|
public const CREATE_GIFT_CARD_SERVICE = 'CreateGiftCard';
|
|
/** @var string Service to use: Cancle Gift Card */
|
|
public const CANCEL_GIFT_CARD_SERVICE = 'CancelGiftCard';
|
|
/** @var string Service to use: Get Available Funds */
|
|
public const GET_AVAILABLE_FUNDS_SERVICE = 'GetAvailableFunds';
|
|
|
|
/** @var Config Configuration class with all settings */
|
|
private $config;
|
|
|
|
/**
|
|
* Initialize the main AWS class. This class prepares and sends all the actions
|
|
* and returns reponses as defined in in the CreateResponse class
|
|
*
|
|
* @param Config $config
|
|
*/
|
|
public function __construct(Config $config)
|
|
{
|
|
$this->config = $config;
|
|
AmazonDebug::writeLog([__METHOD__ => date('Y-m-d H:m:s.u')]);
|
|
}
|
|
|
|
/**
|
|
* Bug a gift card
|
|
*
|
|
* @param float $amount Amount to buy a gifr card in set currencty
|
|
* @param string|null $creation_id Override creation id, if not set will
|
|
* be created automatically. If not valid error
|
|
* will be thrown
|
|
* @return CreateResponse Object with AWS response data
|
|
*
|
|
* @throws AmazonErrors
|
|
*/
|
|
public function getCode(float $amount, ?string $creation_id = null): CreateResponse
|
|
{
|
|
$service_operation = self::CREATE_GIFT_CARD_SERVICE;
|
|
$payload = $this->getGiftCardPayload($amount, $creation_id);
|
|
$canonical_request = $this->getCanonicalRequest($service_operation, $payload);
|
|
$date_time_string = $this->getTimestamp();
|
|
AmazonDebug::writeLog(['call' => __METHOD__]);
|
|
$result = json_decode($this->makeRequest(
|
|
$payload,
|
|
$canonical_request,
|
|
$service_operation,
|
|
$date_time_string
|
|
), true);
|
|
return new CreateResponse($result);
|
|
}
|
|
|
|
/**
|
|
* Cancle an ordered gift card, only possible within the the time limit
|
|
*
|
|
* @param string $creation_request_id Previously created creation request id
|
|
* @param string $gift_card_id Previously created gift card id
|
|
* @return CancelResponse Object with AWS response data
|
|
*
|
|
* @throws AmazonErrors
|
|
*/
|
|
public function cancelCode(string $creation_request_id, string $gift_card_id): CancelResponse
|
|
{
|
|
$service_operation = self::CANCEL_GIFT_CARD_SERVICE;
|
|
$payload = $this->getCancelGiftCardPayload($creation_request_id, $gift_card_id);
|
|
$canonical_request = $this->getCanonicalRequest($service_operation, $payload);
|
|
$date_time_string = $this->getTimestamp();
|
|
AmazonDebug::writeLog(['call' => __METHOD__]);
|
|
$result = json_decode($this->makeRequest(
|
|
$payload,
|
|
$canonical_request,
|
|
$service_operation,
|
|
$date_time_string
|
|
), true);
|
|
return new CancelResponse($result);
|
|
}
|
|
|
|
/**
|
|
* Get current account funds
|
|
*
|
|
* @return CreateBalanceResponse Object with AWS response data
|
|
*
|
|
* @throws AmazonErrors
|
|
*/
|
|
public function getBalance(): CreateBalanceResponse
|
|
{
|
|
$service_operation = self::GET_AVAILABLE_FUNDS_SERVICE;
|
|
$payload = $this->getAvailableFundsPayload();
|
|
$canonical_request = $this->getCanonicalRequest($service_operation, $payload);
|
|
$date_time_string = $this->getTimestamp();
|
|
AmazonDebug::writeLog(['call' => __METHOD__]);
|
|
$result = json_decode($this->makeRequest(
|
|
$payload,
|
|
$canonical_request,
|
|
$service_operation,
|
|
$date_time_string
|
|
), true);
|
|
return new CreateBalanceResponse($result);
|
|
}
|
|
|
|
/**
|
|
* General request method for all actions
|
|
* Calls the Client class that actually runs the json request
|
|
* For service_operation valid data see AWS GCOD documentation
|
|
*
|
|
* @param string $payload The data needed for this request
|
|
* @param string $canonical_request Header data to send for this request
|
|
* @param string $service_operation Service operation. CREATE_GIFT_CARD_SERVICE,
|
|
* CANCEL_GIFT_CARD_SERVICE or
|
|
* GET_AVAILABLE_FUNDS_SERVICE constant values
|
|
* @param string $date_time_string Ymd\THis\Z encoded timestamp, getTimestamp()
|
|
* @return string Request result as string, json data
|
|
*/
|
|
public function makeRequest(
|
|
string $payload,
|
|
string $canonical_request,
|
|
string $service_operation,
|
|
string $date_time_string
|
|
): string {
|
|
// debug
|
|
AmazonDebug::writeLog([__METHOD__ => [
|
|
'Operation' => $service_operation,
|
|
'Payload' => $payload,
|
|
'Cannonical Request' => $canonical_request,
|
|
'Date Time String' => $date_time_string
|
|
|
|
]]);
|
|
$KEY_QUALIFIER = self::KEY_QUALIFIER;
|
|
$canonical_request_hash = $this->buildHash($canonical_request);
|
|
$string_to_sign = $this->buildStringToSign($canonical_request_hash);
|
|
$authorization_value = $this->buildAuthSignature($string_to_sign);
|
|
|
|
$secret_key = $this->config->getSecret();
|
|
$endpoint = $this->config->getEndpoint();
|
|
$region_name = $this->getRegion();
|
|
|
|
$SERVICE_NAME = 'AGCODService';
|
|
$service_target = 'com.amazonaws.agcod.' . $SERVICE_NAME . '.' . $service_operation;
|
|
$date_string = $this->getDateString();
|
|
|
|
$signature_aws_key = $KEY_QUALIFIER . $secret_key;
|
|
|
|
$k_date = $this->hmac($date_string, $signature_aws_key);
|
|
$k_date_hexis = $this->hmac($date_string, $signature_aws_key, false);
|
|
$k_region = $this->hmac($region_name, $k_date);
|
|
$k_region_hexis = $this->hmac($region_name, $k_date, false);
|
|
$k_service_hexis = $this->hmac($SERVICE_NAME, $k_region, false);
|
|
|
|
AmazonDebug::writeLog([__METHOD__ => [
|
|
'Date' => $k_date_hexis,
|
|
'Region' => $k_region_hexis,
|
|
'Service' => $k_service_hexis,
|
|
]]);
|
|
|
|
$url = 'https://' . $endpoint . '/' . $service_operation;
|
|
$headers = $this->buildHeaders(
|
|
$payload,
|
|
$authorization_value,
|
|
$date_time_string,
|
|
$service_target
|
|
);
|
|
return (new Client())->request($url, $headers, $payload);
|
|
}
|
|
|
|
/**
|
|
* Build the headers used in the makeRequest method.
|
|
* These are the HTML headers used with curl
|
|
*
|
|
* @param string $payload Paylout to create this header for
|
|
* @param string $authorization_value Auth string
|
|
* @param string $date_time_string Ymd\THis\Z encoded timestamp, getTimestamp()
|
|
* @param string $service_target Target service in the agcod string:
|
|
* Value like com.amazonaws.agcod.<sn>.<so>
|
|
* @return array<mixed> Header data as array for curl request
|
|
*/
|
|
public function buildHeaders(
|
|
string $payload,
|
|
string $authorization_value,
|
|
string $date_time_string,
|
|
string $service_target
|
|
): array {
|
|
$ACCEPT_HEADER = self::ACCEPT_HEADER;
|
|
$X_AMZ_DATE_HEADER = self::X_AMZ_DATE_HEADER;
|
|
$X_AMZ_TARGET_HEADER = self::X_AMZ_TARGET_HEADER;
|
|
$AUTHORIZATION_HEADER = self::AUTHORIZATION_HEADER;
|
|
return [
|
|
'Content-Type:' . $this->getContentType(),
|
|
'Content-Length: ' . strlen($payload),
|
|
$AUTHORIZATION_HEADER . ':' . $authorization_value,
|
|
$X_AMZ_DATE_HEADER . ':' . $date_time_string,
|
|
$X_AMZ_TARGET_HEADER . ':' . $service_target,
|
|
$ACCEPT_HEADER . ':' . $this->getContentType()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* The request string build with the actauly request data created by
|
|
* getCanonicalRequest(). This string is used in the auth signature call
|
|
*
|
|
* @param string $canonical_request_hash sha256 hash to build from
|
|
* @return string String to send to buildAuthSignature()
|
|
*/
|
|
public function buildStringToSign($canonical_request_hash): string
|
|
{
|
|
$AWS_SHA256_ALGORITHM = self::AWS_SHA256_ALGORITHM;
|
|
$TERMINATION_STRING = self::TERMINATION_STRING;
|
|
$SERVICE_NAME = self::SERVICE_NAME;
|
|
$region_name = $this->getRegion();
|
|
$date_time_string = $this->getTimestamp();
|
|
$date_string = $this->getDateString();
|
|
$string_to_sign = "$AWS_SHA256_ALGORITHM\n"
|
|
. "$date_time_string\n"
|
|
. "$date_string/$region_name/$SERVICE_NAME/$TERMINATION_STRING\n"
|
|
. "$canonical_request_hash";
|
|
|
|
return $string_to_sign;
|
|
}
|
|
|
|
/**
|
|
* Build the authentication signature used in the buildHeaders method
|
|
*
|
|
* @param string $string_to_sign Data to sign, buildStringToSign()
|
|
* @return string Authorized value as string
|
|
*/
|
|
public function buildAuthSignature(string $string_to_sign): string
|
|
{
|
|
$AWS_SHA256_ALGORITHM = self::AWS_SHA256_ALGORITHM;
|
|
$SERVICE_NAME = self::SERVICE_NAME;
|
|
$TERMINATION_STRING = self::TERMINATION_STRING;
|
|
$ACCEPT_HEADER = self::ACCEPT_HEADER;
|
|
$HOST_HEADER = self::HOST_HEADER;
|
|
$X_AMZ_DATE_HEADER = self::X_AMZ_DATE_HEADER;
|
|
$X_AMZ_TARGET_HEADER = self::X_AMZ_TARGET_HEADER;
|
|
|
|
$aws_key_id = $this->config->getAccessKey();
|
|
$region_name = $this->getRegion();
|
|
|
|
$date_string = $this->getDateString();
|
|
$derived_key = $this->buildDerivedKey();
|
|
// Calculate signature per http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
|
$final_signature = $this->hmac($string_to_sign, $derived_key, false);
|
|
|
|
// Assemble Authorization Header with signing information
|
|
// per http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
|
|
$authorization_value =
|
|
$AWS_SHA256_ALGORITHM
|
|
. ' Credential=' . $aws_key_id
|
|
. '/' . $date_string . '/' . $region_name . '/' . $SERVICE_NAME . '/' . $TERMINATION_STRING . ','
|
|
. ' SignedHeaders='
|
|
. $ACCEPT_HEADER . ';' . $HOST_HEADER . ';' . $X_AMZ_DATE_HEADER . ';' . $X_AMZ_TARGET_HEADER . ','
|
|
. ' Signature='
|
|
. $final_signature;
|
|
|
|
return $authorization_value;
|
|
}
|
|
|
|
/**
|
|
* Build the derived key to build the final hmac signature string
|
|
*
|
|
* @param bool $rawOutput Set to true to create the hash based message
|
|
* authenticator string as normal text string or
|
|
* lowercase hexbits
|
|
* @return string Derived key (hmac type)
|
|
*/
|
|
public function buildDerivedKey(bool $rawOutput = true): string
|
|
{
|
|
$KEY_QUALIFIER = self::KEY_QUALIFIER;
|
|
$TERMINATION_STRING = self::TERMINATION_STRING;
|
|
$SERVICE_NAME = self::SERVICE_NAME;
|
|
|
|
$aws_secret_key = $this->config->getSecret();
|
|
// Append Key Qualifier, "AWS4", to secret key per
|
|
// shttp://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html
|
|
$signature_aws_key = $KEY_QUALIFIER . $aws_secret_key;
|
|
$region_name = $this->getRegion();
|
|
$date_string = $this->getDateString();
|
|
|
|
$k_date = $this->hmac($date_string, $signature_aws_key);
|
|
$k_region = $this->hmac($region_name, $k_date);
|
|
$k_service = $this->hmac($SERVICE_NAME, $k_region);
|
|
|
|
// Derived the Signing key (derivedKey aka kSigning)
|
|
return $this->hmac($TERMINATION_STRING, $k_service, $rawOutput);
|
|
}
|
|
|
|
/**
|
|
* get the region based on endpoint
|
|
* list as of 2021/10/20
|
|
* WHERE URL REGION
|
|
* North America https://agcod-v2-gamma.amazon.com us-east-1
|
|
* https://agcod-v2.amazon.com
|
|
* (US, CA, MX)
|
|
* Europe and Asia https://agcod-v2-eu-gamma.amazon.com eu-west-1
|
|
* https://agcod-v2-eu.amazon.com
|
|
* (IT, ES, DE, FR, UK, TR, UAE, KSA, PL, NL, SE)
|
|
* Far East https://agcod-v2-fe-gamma.amazon.com us-west-2
|
|
* https://agcod-v2-fe.amazon.com
|
|
* (JP, AU, SG)
|
|
*
|
|
* CURRENCY
|
|
* USD for US
|
|
* EUR for EU (IT, ES, DE, FR, PL, NL, SE)
|
|
* JPY for JP
|
|
* CAD for CA
|
|
* AUD for AU
|
|
* TRY for TR
|
|
* AED for UAE
|
|
* MXN for MX
|
|
* GBP for UK
|
|
*
|
|
* @return string Region string depending on given endpoint url
|
|
*/
|
|
public function getRegion(): string
|
|
{
|
|
$endpoint = $this->config->getEndpoint();
|
|
// default region
|
|
$region_name = 'us-east-1';
|
|
|
|
switch ($endpoint) {
|
|
case 'agcod-v2.amazon.com':
|
|
case 'agcod-v2-gamma.amazon.com':
|
|
$region_name = 'us-east-1';
|
|
break;
|
|
case 'agcod-v2-eu.amazon.com':
|
|
case 'agcod-v2-eu-gamma.amazon.com':
|
|
$region_name = 'us-west-1';
|
|
break;
|
|
case 'agcod-v2-fe.amazon.com':
|
|
case 'agcod-v2-fe-gamma.amazon.com':
|
|
$region_name = 'us-west-2';
|
|
break;
|
|
}
|
|
return $region_name;
|
|
}
|
|
|
|
|
|
/**
|
|
* The actual data to send as json encoded string for creating a gift card.
|
|
* The creation request id must be in the format:
|
|
* <partner_id>_<unique id 13 characters>
|
|
*
|
|
* @param float $amount Amount of currencty to create the gift card
|
|
* request for
|
|
* @param string|null $creation_id The creation id, if not set will be created here
|
|
* @return string JSON encoded array to be used as payload
|
|
* in get gift card call
|
|
*/
|
|
public function getGiftCardPayload(float $amount, ?string $creation_id = null): string
|
|
{
|
|
$payload = [
|
|
'creationRequestId' => $creation_id ?: uniqid($this->config->getPartner() . '_'),
|
|
'partnerId' => $this->config->getPartner(),
|
|
'value' =>
|
|
[
|
|
'currencyCode' => $this->config->getCurrency(),
|
|
'amount' => $amount
|
|
]
|
|
];
|
|
return (json_encode($payload)) ?: '';
|
|
}
|
|
|
|
/**
|
|
* The actual data to send as json encoded string to cancel a created gift card
|
|
*
|
|
* @param string $creation_request_id Creation request id from previous get gift card
|
|
* @param string $gift_card_id Gift card id from previous get gift card
|
|
* @return string JSON encoded array to be used as payload
|
|
* in cancle gift card call
|
|
*/
|
|
public function getCancelGiftCardPayload(string $creation_request_id, string $gift_card_id): string
|
|
{
|
|
$payload = [
|
|
'creationRequestId' => $creation_request_id,
|
|
'partnerId' => $this->config->getPartner(),
|
|
'gcId' => $gift_card_id
|
|
];
|
|
return (json_encode($payload)) ?: '';
|
|
}
|
|
|
|
/**
|
|
* The actualy data to send as json encoded string for getting the current
|
|
* account funds
|
|
*
|
|
* @return string JSON encoded array to be used as payload in funds call
|
|
*/
|
|
public function getAvailableFundsPayload(): string
|
|
{
|
|
$payload = [
|
|
'partnerId' => $this->config->getPartner(),
|
|
];
|
|
return (json_encode($payload)) ?: '';
|
|
}
|
|
|
|
/**
|
|
* Heeders used in the getCanonicalRequest()
|
|
*
|
|
* @param string $service_operation Service operation code in the service string request
|
|
* Value is: com.amazonaws.agcod.AGCODService.<so>
|
|
* @return string Header string to be used
|
|
*/
|
|
public function buildCanonicalHeaders(string $service_operation): string
|
|
{
|
|
$ACCEPT_HEADER = self::ACCEPT_HEADER;
|
|
$HOST_HEADER = self::HOST_HEADER;
|
|
$X_AMZ_DATE_HEADER = self::X_AMZ_DATE_HEADER;
|
|
$X_AMZ_TARGET_HEADER = self::X_AMZ_TARGET_HEADER;
|
|
$date_time_string = $this->getTimestamp();
|
|
$endpoint = $this->config->getEndpoint();
|
|
$content_type = $this->getContentType();
|
|
return "$ACCEPT_HEADER:$content_type\n"
|
|
. "$HOST_HEADER:$endpoint\n"
|
|
. "$X_AMZ_DATE_HEADER:$date_time_string\n"
|
|
. "$X_AMZ_TARGET_HEADER:com.amazonaws.agcod.AGCODService.$service_operation";
|
|
}
|
|
|
|
/**
|
|
* Headers used in the get/cancel/funds requests
|
|
*
|
|
* @param string $service_operation Service operation code to be used in header request
|
|
* and main request call
|
|
* @param string $payload Payload from get/cancle Code or funds call
|
|
* @return string Full POST service request code
|
|
*/
|
|
public function getCanonicalRequest(string $service_operation, string $payload): string
|
|
{
|
|
$HOST_HEADER = self::HOST_HEADER;
|
|
$X_AMZ_DATE_HEADER = self::X_AMZ_DATE_HEADER;
|
|
$X_AMZ_TARGET_HEADER = self::X_AMZ_TARGET_HEADER;
|
|
$ACCEPT_HEADER = self::ACCEPT_HEADER;
|
|
$payload_hash = $this->buildHash($payload);
|
|
$canonical_headers = $this->buildCanonicalHeaders($service_operation);
|
|
$canonical_request = "POST\n"
|
|
. "/$service_operation\n\n"
|
|
. "$canonical_headers\n\n"
|
|
. "$ACCEPT_HEADER;$HOST_HEADER;$X_AMZ_DATE_HEADER;$X_AMZ_TARGET_HEADER\n"
|
|
. "$payload_hash";
|
|
return $canonical_request;
|
|
}
|
|
|
|
/**
|
|
* Build sha256 hash from given data
|
|
*
|
|
* @param string $data Data to be hashed with sha256
|
|
* @return string sha256 hash
|
|
*/
|
|
public function buildHash(string $data): string
|
|
{
|
|
return hash('sha256', $data);
|
|
}
|
|
|
|
/**
|
|
* Create a sha256 based Hash-Based Message Authentication Code
|
|
* with the given key and data
|
|
*
|
|
* @param string $data Data to be hashed with key below
|
|
* @param string $key Key to be used for creating the hash
|
|
* @param bool $raw Returning data as ascii string or hexibits
|
|
* @return string Hash-Based Message Authentication Code
|
|
*/
|
|
public function hmac(string $data, string $key, bool $raw = true): string
|
|
{
|
|
return hash_hmac('sha256', $data, $key, $raw);
|
|
}
|
|
|
|
/**
|
|
* Build timestamp in the format used by AWS services
|
|
* eg 20211009\T102030\Z
|
|
*
|
|
* @return string date string based on current time. Ymd\THis\Z
|
|
*/
|
|
public function getTimestamp()
|
|
{
|
|
return gmdate('Ymd\THis\Z');
|
|
}
|
|
|
|
/**
|
|
* Get only the date string from the getTimestamp
|
|
* eg 20211009
|
|
*
|
|
* @return string Date string YYYYmmdd extracted from getTimestamp()
|
|
*/
|
|
public function getDateString()
|
|
{
|
|
return substr($this->getTimestamp(), 0, 8);
|
|
}
|
|
|
|
/**
|
|
* Fixed content type for submission, is json
|
|
*
|
|
* @return string 'application/json' string
|
|
*/
|
|
public function getContentType(): string
|
|
{
|
|
return 'application/json';
|
|
}
|
|
}
|
|
|
|
// __END__
|