diff --git a/src/Sns/Exception/MessageValidatorException.php b/src/Sns/Exception/MessageValidatorException.php deleted file mode 100644 index 55a7de3a1a..0000000000 --- a/src/Sns/Exception/MessageValidatorException.php +++ /dev/null @@ -1,7 +0,0 @@ - ['Message', 'MessageId', 'Timestamp', 'TopicArn', - 'Type', 'Signature', 'SigningCertURL',], - 'SubscriptionConfirmation' => ['SubscribeURL', 'Token'], - 'UnsubscribeConfirmation' => ['SubscribeURL', 'Token'] - ]; - - private static $signableKeys = ['Message', 'MessageId', 'Subject', - 'SubscribeURL', 'Timestamp', 'Token', 'TopicArn', 'Type']; - - /** @var array The message data */ - private $data; - - /** - * Creates a Message object from an array of raw message data - * - * @param array $data The message data - * - * @return Message - * @throws \InvalidArgumentException If a valid type is not provided or - * there are other required keys missing - */ - public static function fromArray(array $data) - { - // Make sure the type key is set - if (!isset($data['Type'])) { - throw new \InvalidArgumentException('The "Type" key must be ' - . 'provided to instantiate a Message object.'); - } - - // Determine required keys and create a collection from the message data - $requiredKeys = array_merge( - self::$requiredKeys['__default'], - isset(self::$requiredKeys[$data['Type']]) - ? self::$requiredKeys[$data['Type']] - : [] - ); - - foreach ($requiredKeys as $key) { - if (empty($data[$key])) { - throw new \InvalidArgumentException($key . ' is required'); - } - } - - return new self($data); - } - - /** - * Creates a message object from the raw POST data - * - * @return Message - * @throws \RuntimeException If the POST data is absent, or not a valid JSON document - */ - public static function fromRawPostData() - { - if (!isset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE'])) { - throw new \RuntimeException('SNS message type header not provided.'); - } - - $data = json_decode(file_get_contents('php://input'), true); - - if (!is_array($data)) { - throw new \RuntimeException('POST data invalid'); - } - - return self::fromArray($data); - } - - /** - * @param array $data Message data with all required keys. - */ - public function __construct(array $data) - { - $this->data = $data; - } - - /** - * Get the entire message data as an array. - * - * @return array - */ - public function getData() - { - return $this->data; - } - - /** - * Gets a single key from the message data - * - * @param string $key Key to retrieve - * - * @return string - */ - public function get($key) - { - return isset($this->data[$key]) ? $this->data[$key] : null; - } - - /** - * Builds a newline delimited string to sign according to the specs - * - * @return string - * @link http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html - */ - public function getStringToSign() - { - $stringToSign = ''; - foreach (self::$signableKeys as $key) { - if ($value = $this->get($key)) { - $stringToSign .= "{$key}\n{$value}\n"; - } - } - - return $stringToSign; - } -} diff --git a/src/Sns/MessageValidator/MessageValidator.php b/src/Sns/MessageValidator/MessageValidator.php deleted file mode 100644 index 51034d77b0..0000000000 --- a/src/Sns/MessageValidator/MessageValidator.php +++ /dev/null @@ -1,117 +0,0 @@ -client = $httpHandler ?: \Aws\default_http_handler(); - } - - /** - * Validates a message from SNS to ensure that it was delivered by AWS - * - * @param Message $message The message to validate - * - * @throws MessageValidatorException If the certificate cannot be - * retrieved, if the certificate's source cannot be verified, or if the - * message's signature is invalid. - */ - public function validate(Message $message) - { - // Get and validate the URL for the certificate. - $certUrl = new Uri($message->get('SigningCertURL')); - $this->validateUrl($certUrl); - - // Get the cert itself and extract the public key - $request = new Request('GET', (string) $certUrl); - $promise = call_user_func($this->client, $request); - $certificate = (string) $promise->wait()->getBody(); - - $key = openssl_get_publickey($certificate); - if (!$key) { - throw new MessageValidatorException('Cannot get the public key ' - . 'from the certificate.'); - } - - // Verify the signature of the message - $content = $message->getStringToSign(); - $signature = base64_decode($message->get('Signature')); - - if (!openssl_verify($content, $signature, $key, OPENSSL_ALGO_SHA1)) { - throw new MessageValidatorException('The message signature is ' - . 'invalid.'); - } - } - - /** - * Determines if a message is valid and that is was delivered by AWS. This - * method does not throw exceptions and returns a simple boolean value. - * - * @param Message $message The message to validate - * - * @return bool - */ - public function isValid(Message $message) - { - try { - $this->validate($message); - return true; - } catch (MessageValidatorException $e) { - return false; - } - } - - /** - * Ensures that the url of the certificate is one belonging to AWS, and not - * just something from the amazonaws domain, which includes S3 buckets. - * - * @param UriInterface $uri - * - * @throws MessageValidatorException if the cert url is invalid - */ - private function validateUrl(UriInterface $uri) - { - // The cert URL must be https, a .pem, and match the following pattern. - $hostPattern = '/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/'; - if ($uri->getScheme() !== 'https' - || substr($uri, -4) !== '.pem' - || !preg_match($hostPattern, $uri->getHost()) - ) { - throw new MessageValidatorException('The certificate is located ' - . 'on an invalid domain.'); - } - } -} diff --git a/tests/Sns/MessageValidator/MessageTest.php b/tests/Sns/MessageValidator/MessageTest.php deleted file mode 100644 index fa9d6b9f48..0000000000 --- a/tests/Sns/MessageValidator/MessageTest.php +++ /dev/null @@ -1,165 +0,0 @@ - 'a', - 'MessageId' => 'b', - 'Timestamp' => 'c', - 'TopicArn' => 'd', - 'Type' => 'e', - 'Subject' => 'f', - 'Signature' => 'g', - 'SigningCertURL' => 'h', - 'SubscribeURL' => 'i', - 'Token' => 'j', - ); - - public function testGetters() - { - $message = new Message($this->messageData); - $this->assertInternalType('array', $message->getData()); - - foreach ($this->messageData as $key => $expectedValue) { - $this->assertEquals($expectedValue, $message->get($key)); - } - } - - public function testFactorySucceedsWithGoodData() - { - $this->assertInstanceOf( - 'Aws\Sns\MessageValidator\Message', - Message::fromArray($this->messageData) - ); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testFactoryFailsWithNoType() - { - $data = $this->messageData; - unset($data['Type']); - Message::fromArray($data); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testFactoryFailsWithMissingData() - { - Message::fromArray(array('Type' => 'Notification')); - } - - public function testCanCreateFromRawPost() - { - $_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE'] = 'Notification'; - - // Prep php://input with mocked data - MockPhpStream::setStartingData(json_encode($this->messageData)); - stream_wrapper_unregister('php'); - stream_wrapper_register('php', __NAMESPACE__ . '\MockPhpStream'); - - $message = Message::fromRawPostData(); - $this->assertInstanceOf('Aws\Sns\MessageValidator\Message', $message); - - stream_wrapper_restore("php"); - unset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE']); - } - - /** - * @expectedException \RuntimeException - */ - public function testCreateFromRawPostFailsWithMissingHeader() - { - Message::fromRawPostData(); - } - - /** - * @expectedException \RuntimeException - */ - public function testCreateFromRawPostFailsWithMissingData() - { - $_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE'] = 'Notification'; - Message::fromRawPostData(); - unset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE']); - } - - /** - * @dataProvider getDataForStringToSignTest - */ - public function testBuildsStringToSignCorrectly( - array $messageData, - $expectedSubject, - $expectedStringToSign - ) { - $message = new Message($messageData); - $this->assertEquals($expectedSubject, $message->get('Subject')); - $this->assertEquals($expectedStringToSign, $message->getStringToSign()); - } - - public function getDataForStringToSignTest() - { - $testCases = array(); - - // Test case where one key is not signable - $testCases[0] = array(); - $testCases[0][] = array( - 'TopicArn' => 'd', - 'Message' => 'a', - 'Timestamp' => 'c', - 'Type' => 'e', - 'MessageId' => 'b', - 'FooBar' => 'f', - ); - $testCases[0][] = null; - $testCases[0][] = <<< STRINGTOSIGN -Message -a -MessageId -b -Timestamp -c -TopicArn -d -Type -e - -STRINGTOSIGN; - - // Test case where all keys are signable - $testCases[1] = array(); - $testCases[1][] = array( - 'TopicArn' => 'e', - 'Message' => 'a', - 'Timestamp' => 'd', - 'Type' => 'f', - 'MessageId' => 'b', - 'Subject' => 'c', - ); - $testCases[1][] = 'c'; - $testCases[1][] = <<< STRINGTOSIGN -Message -a -MessageId -b -Subject -c -Timestamp -d -TopicArn -e -Type -f - -STRINGTOSIGN; - - return $testCases; - } -} diff --git a/tests/Sns/MessageValidator/MessageValidatorTest.php b/tests/Sns/MessageValidator/MessageValidatorTest.php deleted file mode 100644 index 7528c7b224..0000000000 --- a/tests/Sns/MessageValidator/MessageValidatorTest.php +++ /dev/null @@ -1,129 +0,0 @@ -markTestSkipped('The OpenSSL extension is required to run ' - . 'the tests for MessageValidator.'); - } - } - - public function testIsValidReturnsFalseOnFailedValidation() - { - $validator = new MessageValidator(); - $message = new Message([]); - $this->assertFalse($validator->isValid($message)); - } - - /** - * @expectedException \Aws\Sns\Exception\MessageValidatorException - * @expectedExceptionMessage The certificate is located on an invalid domain. - */ - public function testValidateFailsWhenCertUrlInvalid() - { - $validator = new MessageValidator(); - $message = new Message([ - 'SigningCertURL' => 'https://foo.amazonaws.com/bar' - ]); - $validator->validate($message); - } - - /** - * @expectedException \Aws\Sns\Exception\MessageValidatorException - * @expectedExceptionMessage Cannot get the public key from the certificate. - */ - public function testValidateFailsWhenCannotDeterminePublicKey() - { - $client = $this->getMockClient(); - $validator = new MessageValidator($client); - $message = new Message([ - 'SigningCertURL' => self::VALID_CERT_URL - ]); - $validator->validate($message); - } - - /** - * @expectedException \Aws\Sns\Exception\MessageValidatorException - * @expectedExceptionMessage The message signature is invalid. - */ - public function testValidateFailsWhenMessageIsInvalid() - { - // Get the signature for some dummy data - list($signature, $certificate) = $this->getSignature('foo'); - // Create the validator with a mock HTTP client that will respond with - // the certificate - $client = $this->getMockClient(new Psr7\Response(200, [], $certificate)); - $validator = new MessageValidator($client); - $message = new Message([ - 'SigningCertURL' => self::VALID_CERT_URL, - 'Signature' => $signature, - ]); - $validator->validate($message); - } - - public function testValidateSucceedsWhenMessageIsValid() - { - // Create a real message - $message = Message::fromArray([ - 'Message' => 'foo', - 'MessageId' => 'bar', - 'Timestamp' => time(), - 'TopicArn' => 'baz', - 'Type' => 'Notification', - 'SigningCertURL' => self::VALID_CERT_URL, - 'Signature' => ' ', - ]); - - // Get the signature for a real message - list($signature, $certificate) = $this->getSignature($message->getStringToSign()); - $ref = new \ReflectionProperty($message, 'data'); - $ref->setAccessible(true); - $ref->setValue($message, ['Signature' => $signature] + $ref->getValue($message)); - - // Create the validator with a mock HTTP client that will respond with - // the certificate - $client = $this->getMockClient(new Psr7\Response(200, [], $certificate)); - $validator = new MessageValidator($client); - - // The message should validate - $this->assertTrue($validator->isValid($message)); - } - - protected function getMockClient(Psr7\Response $response = null) - { - $response = $response ?: new Psr7\Response(200); - - return function () use ($response) { - return \GuzzleHttp\Promise\promise_for($response); - }; - } - - protected function getSignature($stringToSign) - { - // Generate a new Certificate Signing Request and public/private keypair - $csr = openssl_csr_new(array(), $keypair); - // Create the self-signed certificate - $x509 = openssl_csr_sign($csr, null, $keypair, 1); - openssl_x509_export($x509, $certificate); - // Create the signature - $privateKey = openssl_get_privatekey($keypair); - openssl_sign($stringToSign, $signature, $privateKey); - // Free the openssl resources used - openssl_pkey_free($keypair); - openssl_x509_free($x509); - - return [base64_encode($signature), Psr7\stream_for($certificate)]; - } -} diff --git a/tests/Sns/MessageValidator/MockPhpStream.php b/tests/Sns/MessageValidator/MockPhpStream.php deleted file mode 100644 index 2abc6f8bd2..0000000000 --- a/tests/Sns/MessageValidator/MockPhpStream.php +++ /dev/null @@ -1,60 +0,0 @@ -data = self::$startingData; - $this->index = 0; - $this->length = strlen(self::$startingData); - } - - public function stream_open($path, $mode, $options, &$opened_path) - { - return true; - } - - public function stream_close() - { - } - - public function stream_stat() - { - return array(); - } - - public function stream_flush() - { - return true; - } - - public function stream_read($count) - { - $length = min($count, $this->length - $this->index); - $data = substr($this->data, $this->index); - $this->index = $this->index + $length; - - return $data; - } - - public function stream_eof() - { - return ($this->index >= $this->length); - } - - public function stream_write($data) - { - return 0; - } -}