422 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace PayWithAmazon;
 | |
| 
 | |
| // Exit if accessed directly
 | |
| defined( 'ABSPATH' ) || exit;
 | |
| 
 | |
| /* 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 'Interface.php';
 | |
| class IpnHandler implements IpnHandlerInterface
 | |
| {
 | |
| 
 | |
|     private $headers = null;
 | |
|     private $body = null;
 | |
|     private $snsMessage = null;
 | |
|     private $fields = array();
 | |
|     private $signatureFields = array();
 | |
|     private $certificate = null;
 | |
|     private $expectedCnName = 'sns.amazonaws.com';
 | |
| 
 | |
|     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 = 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);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /* 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";
 | |
| 
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /* Verify that the signature is correct for the given data and
 | |
|      * public key
 | |
|      *
 | |
|      * @param string $data            data to validate
 | |
|      * @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->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 $data            data to validate
 | |
|      * @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();
 | |
| 
 | |
|         // 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'],
 | |
|                             'IsSample' =>$ipnMessage['IsSample'],
 | |
|                             'SellerId' =>$ipnMessage['SellerId'],
 | |
|                             'ReleaseEnvironment' =>$ipnMessage['ReleaseEnvironment'],
 | |
|                             'Version' =>$ipnMessage['Version']);
 | |
| 
 | |
|         return $remainingFields;
 | |
|     }
 | |
| }
 |