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.. * @return array 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: * _ * * @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. * @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__