HEX
Server: Apache/2.4.41 (Ubuntu)
System: Linux ip-172-31-42-149 5.15.0-1084-aws #91~20.04.1-Ubuntu SMP Fri May 2 07:00:04 UTC 2025 aarch64
User: ubuntu (1000)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/vhost/disk-apps/magento.bikenow.co/vendor/amzn/amazon-pay-sdk-php/AmazonPay/IpnHandler.php
<?php
namespace AmazonPay;

/* Class IPN_Handler
 * Takes headers and body of the IPN message as input in the constructor
 * verifies that the IPN is from the right resource and has the valid data
 */

require_once 'HttpCurl.php';
require_once 'IpnHandlerInterface.php';
if (!interface_exists('\Psr\Log\LoggerAwareInterface')) {
    require_once(__DIR__.'/../Psr/Log/LoggerAwareInterface.php');
}
if (!interface_exists('\Psr\Log\LoggerInterface')) {
    require_once(__DIR__.'/../Psr/Log/LoggerInterface.php');
}
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;

class IpnHandler implements IpnHandlerInterface, LoggerAwareInterface
{

    private $headers = null;
    private $body = null;
    private $snsMessage = null;
    private $fields = array();
    private $signatureFields = array();
    private $certificate = null;
    private $expectedCnName = 'sns.amazonaws.com';
    private $defaultHostPattern = '/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/';

    // Implement a logging library that utilizes the PSR 3 logger interface
    private $logger = null;

    private $ipnConfig = array('cabundle_file'  => null,
                   'proxy_host'     => null,
                               'proxy_port'     => -1,
                               'proxy_username' => null,
                   'proxy_password' => null);


    public function __construct($headers, $body, $ipnConfig = null)
    {
        $this->headers = array_change_key_case($headers, CASE_LOWER);
        $this->body = $body;

        if ($ipnConfig != null) {
            $this->checkConfigKeys($ipnConfig);
        }

        // Get the list of fields that we are interested in
        $this->fields = array(
            "Timestamp" => true,
            "Message" => true,
            "MessageId" => true,
            "Subject" => false,
            "TopicArn" => true,
            "Type" => true
        );

        // Validate the IPN message header [x-amz-sns-message-type]
        $this->validateHeaders();

        // Converts the IPN [Message] to Notification object
        $this->getMessage();

        // Checks if the notification [Type] is Notification and constructs the signature fields
        $this->checkForCorrectMessageType();

        // Verifies the signature against the provided pem file in the IPN
        $this->constructAndVerifySignature();
    }

    private function checkConfigKeys($ipnConfig)
    {
        $ipnConfig = array_change_key_case($ipnConfig, CASE_LOWER);
    $ipnConfig = $this->trimArray($ipnConfig);

        foreach ($ipnConfig as $key => $value) {
            if (array_key_exists($key, $this->ipnConfig)) {
                $this->ipnConfig[$key] = $value;
            } else {
                throw new \Exception('Key ' . $key . ' is either not part of the configuration or has incorrect Key name.
                check the ipnConfig array key names to match your key names of your config array ', 1);
            }
        }
    }

    public function setLogger(LoggerInterface $logger = null) {
        $this->logger = $logger;
    }
    
    /* Helper function to log data within the Client */

    private function logMessage($message) {
        if ($this->logger) {
            $this->logger->debug($message);
        }
    }

    /* Setter function
     * Sets the value for the key if the key exists in ipnConfig
     */
    
    public function __set($name, $value)
    {
        if (array_key_exists(strtolower($name), $this->ipnConfig)) {
            $this->ipnConfig[$name] = $value;
        } else {
            throw new \Exception("Key " . $name . " is not part of the configuration", 1);
        }
    }

    /* Getter function
     * Returns the value for the key if the key exists in ipnConfig
     */
    
    public function __get($name)
    {
        if (array_key_exists(strtolower($name), $this->ipnConfig)) {
            return $this->ipnConfig[$name];
        } else {
            throw new \Exception("Key " . $name . " was not found in the configuration", 1);
        }
    }

    /* Trim the input Array key values */
    
    private function trimArray($array)
    {
    foreach ($array as $key => $value)
    {
        $array[$key] = trim($value);
    }
    return $array;
    }
    
    private function validateHeaders()
    {
        // Quickly check that this is a sns message
        if (!array_key_exists('x-amz-sns-message-type', $this->headers)) {
            throw new \Exception("Error with message - header " . "does not contain x-amz-sns-message-type header");
        }

        if ($this->headers['x-amz-sns-message-type'] !== 'Notification') {
            throw new \Exception("Error with message - header x-amz-sns-message-type is not " . "Notification, is " . $this->headers['x-amz-sns-message-type']);
        }
    }

    private function getMessage()
    {
        $this->snsMessage = json_decode($this->body, true);

        $json_error = json_last_error();

        if ($json_error != 0) {
            $errorMsg = "Error with message - content is not in json format" . $this->getErrorMessageForJsonError($json_error) . " " . $this->snsMessage;
            throw new \Exception($errorMsg);
        }
    }

    /* Convert a json error code to a descriptive error message
     *
     * @param int $json_error message code
     *
     * @return string error message
     */
    
    private function getErrorMessageForJsonError($json_error)
    {
        switch ($json_error) {
            case JSON_ERROR_DEPTH:
                return " - maximum stack depth exceeded.";
                break;
            case JSON_ERROR_STATE_MISMATCH:
                return " - invalid or malformed JSON.";
                break;
            case JSON_ERROR_CTRL_CHAR:
                return " - control character error.";
                break;
            case JSON_ERROR_SYNTAX:
                return " - syntax error.";
                break;
            default:
                return ".";
                break;
        }
    }

    /* checkForCorrectMessageType()
     *
     * Checks if the Field [Type] is set to ['Notification']
     * Gets the value for the fields marked true in the fields array
     * Constructs the signature string
     */

    private function checkForCorrectMessageType()
    {
        $type = $this->getMandatoryField("Type");
        if (strcasecmp($type, "Notification") != 0) {
            throw new \Exception("Error with SNS Notification - unexpected message with Type of " . $type);
        }

        if (strcmp($this->getMandatoryField("Type"), "Notification") != 0) {
            throw new \Exception("Error with signature verification - unable to verify " . $this->getMandatoryField("Type") . " message");
        } else {

            // Sort the fields into byte order based on the key name(A-Za-z)
            ksort($this->fields);

            // Extract the key value pairs and sort in byte order
            $signatureFields = array();
            foreach ($this->fields as $fieldName => $mandatoryField) {
                if ($mandatoryField) {
                    $value = $this->getMandatoryField($fieldName);
                } else {
                    $value = $this->getField($fieldName);
                }

                if (!is_null($value)) {
                    array_push($signatureFields, $fieldName);
                    array_push($signatureFields, $value);
                }
            }

            /* Create the signature string - key / value in byte order
             * delimited by newline character + ending with a new line character
             */
            $this->signatureFields = implode("\n", $signatureFields) . "\n";
        }
    }

    /* Ensures that the URL of the certificate is one belonging to AWS.
     *
     * @param string $url Certificate URL
     *
     * @throws InvalidSnsMessageException if the cert url is invalid.
     */

    private function validateUrl($url)
    {
        $parsed = parse_url($url);
        if (empty($parsed['scheme'])
            || empty($parsed['host'])
            || $parsed['scheme'] !== 'https'
            || substr($url, -4) !== '.pem'
            || !preg_match($this->defaultHostPattern, $parsed['host'])
        ) {
            throw new \Exception(
                'The certificate is located on an invalid domain.'
            );
        }
    }

    /* Verify that the signature is correct for the given data and
     * public key
     *
     * @param string $signature       decoded signature to compare against
     * @param string $certificatePath path to certificate, can be file or url
     *
     * @throws Exception if there is an error with the call
     *
     * @return bool true if valid
     */
    
    private function constructAndVerifySignature()
    {
    $signature       = base64_decode($this->getMandatoryField("Signature"));
        $certificatePath = $this->getMandatoryField("SigningCertURL");
        $this->validateUrl($certificatePath);
        $this->certificate = $this->getCertificate($certificatePath);

        $result = $this->verifySignatureIsCorrectFromCertificate($signature);
        if (!$result) {
            throw new \Exception("Unable to match signature from remote server: signature of " . $this->getCertificate($certificatePath) . " , SigningCertURL of " . $this->getMandatoryField("SigningCertURL") . " , SignatureOf " . $this->getMandatoryField("Signature"));
        }
    }

    /* getCertificate($certificatePath)
     *
     * gets the certificate from the $certificatePath using Curl
     */
    
    private function getCertificate($certificatePath)
    {
        $httpCurlRequest  = new HttpCurl($this->ipnConfig);

    $response = $httpCurlRequest->httpGet($certificatePath);

        return $response;
    }

    /* Verify that the signature is correct for the given data and public key
     *
     * @param string $signature       decoded signature to compare against
     * @param string $certificate     certificate object defined in Certificate.php
     */

    public function verifySignatureIsCorrectFromCertificate($signature)
    {
        $certKey = openssl_get_publickey($this->certificate);

        if ($certKey === False) {
            throw new \Exception("Unable to extract public key from cert");
        }

        try {
            $certInfo    = openssl_x509_parse($this->certificate, true);
            $certSubject = $certInfo["subject"];

            if (is_null($certSubject)) {
                throw new \Exception("Error with certificate - subject cannot be found");
            }
        } catch (\Exception $ex) {
            throw new \Exception("Unable to verify certificate - error with the certificate subject", null, $ex);
        }

        if (strcmp($certSubject["CN"], $this->expectedCnName)) {
            throw new \Exception("Unable to verify certificate issued by Amazon - error with certificate subject");
        }

        $result = -1;
        try {
            $result = openssl_verify($this->signatureFields, $signature, $certKey, OPENSSL_ALGO_SHA1);
        } catch (\Exception $ex) {
            throw new \Exception("Unable to verify signature - error with the verification algorithm", null, $ex);
        }

        return ($result > 0);
    }


    /* Extract the mandatory field from the message and return the contents
     *
     * @param string $fieldName name of the field to extract
     *
     * @throws Exception if not found
     *
     * @return string field contents if found
     */
    
    private function getMandatoryField($fieldName)
    {
        $value = $this->getField($fieldName);
        if (is_null($value)) {
            throw new \Exception("Error with json message - mandatory field " . $fieldName . " cannot be found");
        }
        return $value;
    }

    /* Extract the field if present, return null if not defined
     *
     * @param string $fieldName name of the field to extract
     *
     * @return string field contents if found, null otherwise
     */
    
    private function getField($fieldName)
    {
        if (array_key_exists($fieldName, $this->snsMessage)) {
            return $this->snsMessage[$fieldName];
        } else {
            return null;
        }
    }

    /* returnMessage() - JSON decode the raw [Message] portion of the IPN */
    
    public function returnMessage()
    {
        return json_decode($this->snsMessage['Message'], true);
    }

    /* toJson() - Converts IPN [Message] field to JSON
     *
     * Has child elements
     * ['NotificationData'] [XML] - API call XML notification data
     * @param remainingFields - consists of remaining IPN array fields that are merged
     * Type - Notification
     * MessageId -  ID of the Notification
     * Topic ARN - Topic of the IPN
     * @return response in JSON format
     */
    
    public function toJson()
    {
        $response = $this->simpleXmlObject();

        // Merging the remaining fields with the response
        $remainingFields = $this->getRemainingIpnFields();
        $responseArray = array_merge($remainingFields,(array)$response);

        // Converting to JSON format
        $response = json_encode($responseArray);

        return $response;
    }

    /* toArray() - Converts IPN [Message] field to associative array
     * @return response in array format
     */
    
    public function toArray()
    {
        $response = $this->simpleXmlObject();

        // Converting the SimpleXMLElement Object to array()
        $response = json_encode($response);
        $response = json_decode($response, true);

        // Merging the remaining fields with the response array
        $remainingFields = $this->getRemainingIpnFields();
        $response = array_merge($remainingFields,$response);

        return $response;
    }

    /* addRemainingFields() - Add remaining fields to the datatype
     *
     * Has child elements
     * ['NotificationData'] [XML] - API call XML response data
     * Convert to SimpleXML element object
     * Type - Notification
     * MessageId -  ID of the Notification
     * Topic ARN - Topic of the IPN
     * @return response in array format
     */

    private function simpleXmlObject()
    {
        $ipnMessage = $this->returnMessage();

        $this->logMessage(sprintf('IPN received for merchant account: %s', $this->sanitizeResponseData($ipnMessage['SellerId'])));
        $this->logMessage($this->sanitizeResponseData($ipnMessage['NotificationData']));

        // Getting the Simple XML element object of the IPN XML Response Body
        $response = simplexml_load_string((string) $ipnMessage['NotificationData']);

        // Adding the Type, MessageId, TopicArn details of the IPN to the Simple XML element Object
        $response->addChild('Type', $this->snsMessage['Type']);
        $response->addChild('MessageId', $this->snsMessage['MessageId']);
        $response->addChild('TopicArn', $this->snsMessage['TopicArn']);

        return $response;
    }

    /* getRemainingIpnFields()
     * Gets the remaining fields of the IPN to be later appended to the return message
     */
    
    private function getRemainingIpnFields()
    {
        $ipnMessage = $this->returnMessage();

        $remainingFields = array(
                            'NotificationReferenceId' =>$ipnMessage['NotificationReferenceId'],
                            'NotificationType' =>$ipnMessage['NotificationType'],
                            'SellerId' =>$ipnMessage['SellerId'],
                            'ReleaseEnvironment' =>$ipnMessage['ReleaseEnvironment'] );

        return $remainingFields;
    }

    private function sanitizeResponseData($input)
    {

        $patterns = array();
        $patterns[0] = '/(<SellerNote>)(.+)(<\/SellerNote>)/ms';
        $patterns[1] = '/(<AuthorizationBillingAddress>)(.+)(<\/AuthorizationBillingAddress>)/ms';
        $patterns[2] = '/(<SellerAuthorizationNote>)(.+)(<\/SellerAuthorizationNote>)/ms';
        $patterns[3] = '/(<SellerCaptureNote>)(.+)(<\/SellerCaptureNote>)/ms';
        $patterns[4] = '/(<SellerRefundNote>)(.+)(<\/SellerRefundNote>)/ms';

        $replacements = array();
        $replacements[0] = '$1 REMOVED $3';
        $replacements[1] = '$1 REMOVED $3';
        $replacements[2] = '$1 REMOVED $3';
        $replacements[3] = '$1 REMOVED $3';
        $replacements[4] = '$1 REMOVED $3';

        return preg_replace($patterns, $replacements, $input);
    }
}