├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── documentation.yml │ └── feature-request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── handle-stale-discussions.yml │ ├── issue-regression-labeler.yml │ └── stale_issues.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Exception │ └── InvalidSnsMessageException.php ├── Message.php └── MessageValidator.php └── tests ├── FunctionalValidationsTest.php ├── MessageTest.php ├── MessageValidatorTest.php └── MockPhpStream.php /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | description: Report a bug 4 | title: "(short issue description)" 5 | labels: [bug, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the bug 12 | description: What is the problem? A clear and concise description of the bug. 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | id: regression 17 | attributes: 18 | label: Regression Issue 19 | description: What is a regression? If it worked in a previous version but doesn't in the latest version, it's considered a regression. In this case, please provide specific version number in the report. 20 | options: 21 | - label: Select this option if this issue appears to be a regression. 22 | required: false 23 | - type: textarea 24 | id: expected 25 | attributes: 26 | label: Expected Behavior 27 | description: | 28 | What did you expect to happen? 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: current 33 | attributes: 34 | label: Current Behavior 35 | description: | 36 | What actually happened? 37 | 38 | Please include full errors, uncaught exceptions, stack traces, and relevant logs. 39 | If service responses are relevant, please include wire logs. 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: reproduction 44 | attributes: 45 | label: Reproduction Steps 46 | description: | 47 | Provide a self-contained, concise snippet of code that can be used to reproduce the issue. 48 | For more complex issues provide a repo with the smallest sample that reproduces the bug. 49 | 50 | Avoid including business logic or unrelated code, it makes diagnosis more difficult. 51 | The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: solution 56 | attributes: 57 | label: Possible Solution 58 | description: | 59 | Suggest a fix/reason for the bug 60 | validations: 61 | required: false 62 | - type: textarea 63 | id: context 64 | attributes: 65 | label: Additional Information/Context 66 | description: | 67 | Anything else that might be relevant for troubleshooting this bug. Providing context helps us come up with a solution that is most useful in the real world. 68 | validations: 69 | required: false 70 | - type: input 71 | id: sdk-version 72 | attributes: 73 | label: SDK version used 74 | validations: 75 | required: true 76 | - type: input 77 | id: environment 78 | attributes: 79 | label: Environment details (OS name and version, etc.) 80 | validations: 81 | required: true 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: 💬 General Question 5 | url: https://github.com/aws/aws-php-sns-message-validator/discussions/categories/q-a 6 | about: Please ask and answer questions as a discussion thread -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📕 Documentation Issue" 3 | description: Report an issue in the API Reference documentation or Developer Guide 4 | title: "(short issue description)" 5 | labels: [documentation, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the issue 12 | description: A clear and concise description of the issue. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: links 18 | attributes: 19 | label: Links 20 | description: | 21 | Include links to affected documentation page(s). 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | description: Suggest an idea for this project 4 | title: "(short issue description)" 5 | labels: [feature-request, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the feature 12 | description: A clear and concise description of the feature you are proposing. 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: use-case 17 | attributes: 18 | label: Use Case 19 | description: | 20 | Why do you need this feature? For example: "I'm always frustrated when..." 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: solution 25 | attributes: 26 | label: Proposed Solution 27 | description: | 28 | Suggest how to implement the addition or change. Please include prototype/workaround/sketch/reference implementation. 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: other 33 | attributes: 34 | label: Other Information 35 | description: | 36 | Any alternative solutions or features you considered, a more detailed explanation, stack traces, related issues, links for context, etc. 37 | validations: 38 | required: false 39 | - type: checkboxes 40 | id: ack 41 | attributes: 42 | label: Acknowledgements 43 | options: 44 | - label: I may be able to implement this feature request 45 | required: false 46 | - label: This feature might incur a breaking change 47 | required: false 48 | - type: input 49 | id: sdk-version 50 | attributes: 51 | label: SDK version used 52 | validations: 53 | required: true 54 | - type: input 55 | id: environment 56 | attributes: 57 | label: Environment details (OS name and version, etc.) 58 | validations: 59 | required: true 60 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | php-versions: ['8.1', '8.2', '8.3', '8.4'] 19 | name: Run tests on PHP ${{ matrix.php-versions }} 20 | 21 | steps: 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | extensions: json 27 | 28 | - uses: actions/checkout@v4 29 | 30 | - name: Validate composer.json and composer.lock 31 | run: composer validate --strict 32 | 33 | - name: Cache Composer packages 34 | id: composer-cache 35 | uses: actions/cache@v3 36 | with: 37 | path: vendor 38 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-php- 41 | 42 | - name: Install dependencies 43 | run: composer install --prefer-dist --no-progress 44 | 45 | - name: Run test suite 46 | run: composer run-script test 47 | -------------------------------------------------------------------------------- /.github/workflows/handle-stale-discussions.yml: -------------------------------------------------------------------------------- 1 | name: HandleStaleDiscussions 2 | on: 3 | schedule: 4 | - cron: '0 */4 * * *' 5 | discussion_comment: 6 | types: [created] 7 | 8 | jobs: 9 | handle-stale-discussions: 10 | if: github.repository_owner == 'aws' 11 | name: Handle stale discussions 12 | runs-on: ubuntu-latest 13 | permissions: 14 | discussions: write 15 | steps: 16 | - name: Stale discussions action 17 | uses: aws-github-ops/handle-stale-discussions@v1 18 | env: 19 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 20 | -------------------------------------------------------------------------------- /.github/workflows/issue-regression-labeler.yml: -------------------------------------------------------------------------------- 1 | # Apply potential regression label on issues 2 | name: issue-regression-label 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | jobs: 7 | add-regression-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | steps: 12 | - name: Fetch template body 13 | id: check_regression 14 | uses: actions/github-script@v7 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | TEMPLATE_BODY: ${{ github.event.issue.body }} 18 | with: 19 | script: | 20 | const regressionPattern = /\[x\] Select this option if this issue appears to be a regression\./i; 21 | const template = `${process.env.TEMPLATE_BODY}` 22 | const match = regressionPattern.test(template); 23 | core.setOutput('is_regression', match); 24 | - name: Manage regression label 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | if [ "${{ steps.check_regression.outputs.is_regression }}" == "true" ]; then 29 | gh issue edit ${{ github.event.issue.number }} --add-label "potential-regression" -R ${{ github.repository }} 30 | else 31 | gh issue edit ${{ github.event.issue.number }} --remove-label "potential-regression" -R ${{ github.repository }} 32 | fi 33 | -------------------------------------------------------------------------------- /.github/workflows/stale_issues.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | cleanup: 10 | runs-on: ubuntu-latest 11 | name: Stale issue job 12 | steps: 13 | - uses: aws-actions/stale-issue-cleanup@v3 14 | with: 15 | # Setting messages to an empty string will cause the automation to skip 16 | # that category 17 | ancient-issue-message: We have noticed this issue has not recieved attention in 3 years. We will close this issue for now. If you think this is in error, please feel free to comment and reopen the issue. 18 | stale-issue-message: This issue has not recieved a response in 1 week. If you want to keep this issue open, please just leave a comment below and auto-close will be canceled. 19 | 20 | # These labels are required 21 | stale-issue-label: closing-soon 22 | exempt-issue-label: no-autoclose 23 | stale-pr-label: no-pr-activity 24 | exempt-pr-label: awaiting-approval 25 | response-requested-label: response-requested 26 | 27 | # Don't set closed-for-staleness label to skip closing very old issues 28 | # regardless of label 29 | closed-for-staleness-label: closed-for-staleness 30 | 31 | # Issue timing 32 | days-before-stale: 7 33 | days-before-close: 4 34 | days-before-ancient: 36500 35 | 36 | # If you don't want to mark a issue as being ancient based on a 37 | # threshold of "upvotes", you can set this here. An "upvote" is 38 | # the total number of +1, heart, hooray, and rocket reactions 39 | # on an issue. 40 | minimum-upvotes-to-exempt: 10 41 | 42 | repo-token: ${{ secrets.GITHUB_TOKEN }} 43 | loglevel: DEBUG 44 | # Set dry-run to true to not perform label or close actions. 45 | # dry-run: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | Makefile 3 | /.idea/ 4 | /*.iml 5 | atlassian-ide-plugin.xml 6 | .DS_Store 7 | .swp 8 | .build 9 | composer.lock 10 | vendor/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: php 4 | 5 | php: 6 | - 5.4 7 | - 5.5 8 | - 5.6 9 | - 7.0 10 | - 7.1 11 | - 7.2 12 | - 7.3 13 | - hhvm 14 | - nightly 15 | 16 | matrix: 17 | allow_failures: 18 | - php: hhvm 19 | - php: nightly 20 | 21 | sudo: false 22 | dist: trusty 23 | 24 | install: 25 | - travis_retry composer update --no-interaction --prefer-dist 26 | 27 | script: vendor/bin/phpunit 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any [issues][] or [pull requests][pull-requests] to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | Jump To: 10 | 11 | * [Bug Reports](_#Bug-Reports_) 12 | * [Feature Requests](_#Feature-Requests_) 13 | * [Code Contributions](_#Code-Contributions_) 14 | * [Code of Conduct](_#Code-of-Conduct_) 15 | * [Security issue notifications](_#Security-issue-notifications_) 16 | * [Licensing](_#Licensing_) 17 | 18 | 19 | 20 | ## How to contribute 21 | 22 | *Before you send us a pull request, please be sure that:* 23 | 24 | 1. You're working from the latest source on the master branch. 25 | 1. You check existing open, and recently closed, pull requests to be sure that 26 | someone else hasn't already addressed the problem. 27 | 1. You create an issue before working on a contribution that will take a significant 28 | amount of your time. 29 | 30 | *Creating a Pull Request* 31 | 32 | 1. Fork the repository. 33 | 1. In your fork, make your change in a branch that's based on this repo's master branch. 34 | 1. Commit the change to your fork, using a clear and descriptive commit message. 35 | 1. Create a pull request, answering any questions in the pull request form. 36 | 37 | For contributions that will take a significant amount of time, open a new issue to pitch 38 | your idea before you get started. Explain the problem and describe the content you want to 39 | see added to the documentation. Let us know if you'll write it yourself or if you'd like us 40 | to help. We'll discuss your proposal with you and let you know whether we're likely to 41 | accept it. 42 | 43 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 44 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 45 | 46 | 47 | ## Bug Reports 48 | 49 | Bug reports are accepted through the [Issues][] page. 50 | 51 | Before Submitting: 52 | 53 | * Do a search through the existing issues to make sure it has not already been reported. 54 | If it has, comment your experience or +1 so we prioritize it. 55 | * If possible, upgrade to the latest release of the SDK. It's possible the bug has 56 | already been fixed in the latest version. 57 | 58 | Writing the Bug Report: 59 | 60 | Please ensure that your bug report has the following: 61 | 62 | * A short, descriptive title. Ideally, other community members should be able to get a 63 | good idea of the issue just from reading the title. 64 | * A detailed description of the problem you're experiencing. This should include: 65 | * Expected behavior of the SDK and the actual behavior exhibited. 66 | * Any details of your application environment that may be relevant. 67 | * Debug information, stack trace or logs. 68 | * If you are able to create one, include a Minimal Working Example that reproduces the issue. 69 | * Use Markdown to make the report easier to read; i.e. use code blocks when pasting a 70 | code snippet. 71 | 72 | ## Feature Requests: 73 | 74 | Open an [issue][] with the following: 75 | 76 | * A short, descriptive title. Ideally, other community members should be able to get a 77 | good idea of the feature just from reading the title. 78 | * A detailed description of the the proposed feature. 79 | * Why it should be added to the SDK. 80 | * If possible, example code to illustrate how it should work. 81 | * Use Markdown to make the request easier to read; 82 | * If you intend to implement this feature, indicate that you'd like to the issue to be 83 | assigned to you. 84 | 85 | ## Bug Reports 86 | 87 | Bug reports are accepted through the [Issues][] page. 88 | 89 | Before Submitting: 90 | 91 | * Do a search through the existing issues to make sure it has not already been reported. 92 | If it has, comment your experience or +1 so we prioritize it. 93 | * If possible, upgrade to the latest release of the SDK. It's possible the bug has 94 | already been fixed in the latest version. 95 | 96 | Writing the Bug Report: 97 | 98 | Please ensure that your bug report has the following: 99 | 100 | * A short, descriptive title. Ideally, other community members should be able to get a 101 | good idea of the issue just from reading the title. 102 | * A detailed description of the problem you're experiencing. This should include: 103 | * Expected behavior of the SDK and the actual behavior exhibited. 104 | * Any details of your application environment that may be relevant. 105 | * Debug information, stack trace or logs. 106 | * If you are able to create one, include a Minimal Working Example that reproduces the issue. 107 | * Use Markdown to make the report easier to read; i.e. use code blocks when pasting a 108 | code snippet. 109 | 110 | 111 | ## Code of Conduct 112 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 113 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 114 | opensource-codeofconduct@amazon.com with any additional questions or comments. 115 | 116 | 117 | ## Security issue notifications 118 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 119 | 120 | 121 | ## Licensing 122 | 123 | See the [LICENSE](https://github.com/aws/aws-php-sns-message-validator/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 124 | 125 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 126 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | Version 2.0, January 2004 3 | 4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 5 | 6 | ## 1. Definitions. 7 | 8 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 9 | through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the 12 | License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled 15 | by, or are under common control with that entity. For the purposes of this definition, "control" means 16 | (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract 17 | or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 18 | ownership of such entity. 19 | 20 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 21 | 22 | "Source" form shall mean the preferred form for making modifications, including but not limited to software 23 | source code, documentation source, and configuration files. 24 | 25 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, 26 | including but not limited to compiled object code, generated documentation, and conversions to other media 27 | types. 28 | 29 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, 30 | as indicated by a copyright notice that is included in or attached to the work (an example is provided in the 31 | Appendix below). 32 | 33 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) 34 | the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, 35 | as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not 36 | include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work 37 | and Derivative Works thereof. 38 | 39 | "Contribution" shall mean any work of authorship, including the original version of the Work and any 40 | modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to 41 | Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to 42 | submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of 43 | electronic, verbal, or written communication sent to the Licensor or its representatives, including but not 44 | limited to communication on electronic mailing lists, source code control systems, and issue tracking systems 45 | that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but 46 | excluding communication that is conspicuously marked or otherwise designated in writing by the copyright 47 | owner as "Not a Contribution." 48 | 49 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been 50 | received by Licensor and subsequently incorporated within the Work. 51 | 52 | ## 2. Grant of Copyright License. 53 | 54 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, 55 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare 56 | Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such 57 | Derivative Works in Source or Object form. 58 | 59 | ## 3. Grant of Patent License. 60 | 61 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, 62 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent 63 | license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such 64 | license applies only to those patent claims licensable by such Contributor that are necessarily infringed by 65 | their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such 66 | Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim 67 | or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work 68 | constitutes direct or contributory patent infringement, then any patent licenses granted to You under this 69 | License for that Work shall terminate as of the date such litigation is filed. 70 | 71 | ## 4. Redistribution. 72 | 73 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without 74 | modifications, and in Source or Object form, provided that You meet the following conditions: 75 | 76 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 77 | 78 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 79 | 80 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, 81 | trademark, and attribution notices from the Source form of the Work, excluding those notices that do 82 | not pertain to any part of the Derivative Works; and 83 | 84 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that 85 | You distribute must include a readable copy of the attribution notices contained within such NOTICE 86 | file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one 87 | of the following places: within a NOTICE text file distributed as part of the Derivative Works; within 88 | the Source form or documentation, if provided along with the Derivative Works; or, within a display 89 | generated by the Derivative Works, if and wherever such third-party notices normally appear. The 90 | contents of the NOTICE file are for informational purposes only and do not modify the License. You may 91 | add Your own attribution notices within Derivative Works that You distribute, alongside or as an 92 | addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be 93 | construed as modifying the License. 94 | 95 | You may add Your own copyright statement to Your modifications and may provide additional or different license 96 | terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative 97 | Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the 98 | conditions stated in this License. 99 | 100 | ## 5. Submission of Contributions. 101 | 102 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by 103 | You to the Licensor shall be under the terms and conditions of this License, without any additional terms or 104 | conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate 105 | license agreement you may have executed with Licensor regarding such Contributions. 106 | 107 | ## 6. Trademarks. 108 | 109 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of 110 | the Licensor, except as required for reasonable and customary use in describing the origin of the Work and 111 | reproducing the content of the NOTICE file. 112 | 113 | ## 7. Disclaimer of Warranty. 114 | 115 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor 116 | provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 117 | or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, 118 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the 119 | appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of 120 | permissions under this License. 121 | 122 | ## 8. Limitation of Liability. 123 | 124 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless 125 | required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any 126 | Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential 127 | damages of any character arising as a result of this License or out of the use or inability to use the Work 128 | (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or 129 | any and all other commercial damages or losses), even if such Contributor has been advised of the possibility 130 | of such damages. 131 | 132 | ## 9. Accepting Warranty or Additional Liability. 133 | 134 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, 135 | acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this 136 | License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole 137 | responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold 138 | each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason 139 | of your accepting any such warranty or additional liability. 140 | 141 | END OF TERMS AND CONDITIONS 142 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Amazon SNS Message Validator for PHP 2 | 3 | 4 | 5 | Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"). 8 | You may not use this file except in compliance with the License. 9 | A copy of the License is located at 10 | 11 | 12 | 13 | or in the "license" file accompanying this file. This file is distributed 14 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 15 | express or implied. See the License for the specific language governing 16 | permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon SNS Message Validator for PHP 2 | 3 | [![@awsforphp on Twitter](http://img.shields.io/badge/twitter-%40awsforphp-blue.svg?style=flat)](https://twitter.com/awsforphp) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/aws/aws-php-sns-message-validator.svg?style=flat)](https://packagist.org/packages/aws/aws-php-sns-message-validator) 5 | [![Build Status](https://img.shields.io/travis/aws/aws-php-sns-message-validator.svg?style=flat)](https://travis-ci.org/aws/aws-php-sns-message-validator) 6 | [![Apache 2 License](https://img.shields.io/packagist/l/aws/aws-php-sns-message-validator.svg?style=flat)](http://aws.amazon.com/apache-2-0/) 7 | 8 | The **Amazon SNS Message Validator for PHP** library allows you to validate that 9 | incoming HTTP(S) POST messages are valid Amazon SNS notifications. This library 10 | is standalone and does not depend on the AWS SDK for PHP or Guzzle; however, it 11 | does require PHP 8.1+ and that the OpenSSL PHP extension is installed. 12 | 13 | Jump To: 14 | * [Basic Usage](_#Basic-Usage_) 15 | * [Installation](_#Installation_) 16 | * [About Amazon SNS](_#About-Amazon-SNS_) 17 | * [Handling Messages](_#Handling-Messages_) 18 | * [Testing Locally](_#Testing-Locally_) 19 | * [Contributing](_#Contributing_) 20 | 21 | ## Basic Usage 22 | 23 | To validate a message, you can instantiate a `Message` object from the POST 24 | data using the `Message::fromRawPostData`. This reads the raw POST data from 25 | the [`php://input` stream][php-input], decodes the JSON data, and validates 26 | the message's type and structure. 27 | 28 | Next, you must create an instance of `MessageValidator`, and then use either 29 | the `isValid()` or `validate()`, methods to validate the message. The 30 | message validator checks the `SigningCertURL`, `SignatureVersion`, and 31 | `Signature` to make sure they are valid and consistent with the message data. 32 | 33 | ```php 34 | isValid($message)) { 46 | // do something with the message 47 | } 48 | ``` 49 | 50 | ## Installation 51 | 52 | The SNS Message Validator can be installed via [Composer][]. 53 | 54 | composer require aws/aws-php-sns-message-validator 55 | 56 | ## Getting Help 57 | 58 | Please use these community resources for getting help. We use the GitHub issues for tracking bugs and feature requests and have limited bandwidth to address them. 59 | 60 | * Ask a question on [StackOverflow](https://stackoverflow.com/) and tag it with [`aws-php-sdk`](http://stackoverflow.com/questions/tagged/aws-php-sdk) 61 | * Come join the AWS SDK for PHP [gitter](https://gitter.im/aws/aws-sdk-php) 62 | * Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home/) 63 | * If it turns out that you may have found a bug, please [open an issue](https://github.com/aws/aws-php-sns-message-validator/issues/new/choose) 64 | 65 | ## About Amazon SNS 66 | 67 | [Amazon Simple Notification Service (Amazon SNS)][sns] is a fast, fully-managed, 68 | push messaging service. Amazon SNS can deliver messages to email, mobile devices 69 | (i.e., SMS; iOS, Android and FireOS push notifications), Amazon SQS queues,and 70 | — of course — HTTP/HTTPS endpoints. 71 | 72 | With Amazon SNS, you can setup topics to publish custom messages to subscribed 73 | endpoints. However, SNS messages are used by many of the other AWS services to 74 | communicate information asynchronously about your AWS resources. Some examples 75 | include: 76 | 77 | * Configuring Amazon Glacier to notify you when a retrieval job is complete. 78 | * Configuring AWS CloudTrail to notify you when a new log file has been written. 79 | * Configuring Amazon Elastic Transcoder to notify you when a transcoding job 80 | changes status (e.g., from "Progressing" to "Complete") 81 | 82 | Though you can certainly subscribe your email address to receive SNS messages 83 | from service events like these, your inbox would fill up rather quickly. There 84 | is great power, however, in being able to subscribe an HTTP/HTTPS endpoint to 85 | receive the messages. This allows you to program webhooks for your applications 86 | to easily respond to various events. 87 | 88 | ## Handling Messages 89 | 90 | ### Confirming a Subscription to a Topic 91 | 92 | In order to handle a `SubscriptionConfirmation` message, you must use the 93 | `SubscribeURL` value in the incoming message: 94 | 95 | ```php 96 | use Aws\Sns\Message; 97 | use Aws\Sns\MessageValidator; 98 | use Aws\Sns\Exception\InvalidSnsMessageException; 99 | 100 | // Instantiate the Message and Validator 101 | $message = Message::fromRawPostData(); 102 | $validator = new MessageValidator(); 103 | 104 | // Validate the message and log errors if invalid. 105 | try { 106 | $validator->validate($message); 107 | } catch (InvalidSnsMessageException $e) { 108 | // Pretend we're not here if the message is invalid. 109 | http_response_code(404); 110 | error_log('SNS Message Validation Error: ' . $e->getMessage()); 111 | die(); 112 | } 113 | 114 | // Check the type of the message and handle the subscription. 115 | if ($message['Type'] === 'SubscriptionConfirmation') { 116 | // Confirm the subscription by sending a GET request to the SubscribeURL 117 | file_get_contents($message['SubscribeURL']); 118 | } 119 | ``` 120 | 121 | ### Receiving a Notification 122 | 123 | To receive a notification, use the same code as the preceding example, but 124 | check for the `Notification` message type. 125 | 126 | ```php 127 | if ($message['Type'] === 'Notification') { 128 | // Do whatever you want with the message body and data. 129 | echo $message['MessageId'] . ': ' . $message['Message'] . "\n"; 130 | } 131 | ``` 132 | 133 | The message body will be a string, and will hold whatever data was published 134 | to the SNS topic. 135 | 136 | ### Unsubscribing 137 | 138 | Unsubscribing looks the same as subscribing, except the message type will be 139 | `UnsubscribeConfirmation`. 140 | 141 | ```php 142 | if ($message['Type'] === 'UnsubscribeConfirmation') { 143 | // Unsubscribed in error? You can resubscribe by visiting the endpoint 144 | // provided as the message's SubscribeURL field. 145 | file_get_contents($message['SubscribeURL']); 146 | } 147 | ``` 148 | 149 | ## Testing Locally 150 | 151 | One challenge of using webhooks in a web application is testing the integration 152 | with the service. Testing integrations with SNS notifications can be fairly easy 153 | using tools like [ngrok][] and [PHP's built-in webserver][php-server]. One of 154 | our blog posts, [*Testing Webhooks Locally for Amazon SNS*][blogpost], illustrates 155 | a good technique for testing. 156 | 157 | > **NOTE:** The code samples in the blog post are specific to the message 158 | > validator in Version 2 of the SDK, but can be easily adapted to using this 159 | > version. 160 | 161 | ### Special Thank You 162 | 163 | A special thanks goes out to [Julian Vidal][] who helped create the [initial 164 | implementation][] in Version 2 of the [AWS SDK for PHP][]. 165 | 166 | [php-input]: http://php.net/manual/en/wrappers.php.php#wrappers.php.input 167 | [composer]: https://getcomposer.org/ 168 | [source code]: https://github.com/aws/aws-php-sns-message-validator/archive/master.zip 169 | [sns]: http://aws.amazon.com/sns/ 170 | [ngrok]: https://ngrok.com/ 171 | [php-server]: http://www.php.net/manual/en/features.commandline.webserver.php 172 | [blogpost]: http://blogs.aws.amazon.com/php/post/Tx2CO24DVG9CAK0/Testing-Webhooks-Locally-for-Amazon-SNS 173 | [Julian Vidal]: https://github.com/poisa 174 | [initial implementation]: https://github.com/aws/aws-sdk-php/tree/2.8/src/Aws/Sns/MessageValidator 175 | [AWS SDK for PHP]: https://github.com/aws/aws-sdk-php 176 | 177 | ## Contributing 178 | 179 | We work hard to provide a high-quality and useful SDK for our AWS services, and we greatly value feedback and contributions from our community. Please review our [contributing guidelines](./CONTRIBUTING.md) before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. 180 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws/aws-php-sns-message-validator", 3 | "homepage": "http://aws.amazon.com/sdkforphp", 4 | "description": "Amazon SNS message validation for PHP", 5 | "keywords": ["aws","amazon","sdk","sns","message","webhooks","cloud"], 6 | "type": "library", 7 | "license": "Apache-2.0", 8 | "authors": [ 9 | { 10 | "name": "Amazon Web Services", 11 | "homepage": "http://aws.amazon.com" 12 | } 13 | ], 14 | "support": { 15 | "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", 16 | "issues": "https://github.com/aws/aws-sns-message-validator/issues" 17 | }, 18 | "require": { 19 | "php": ">=8.1", 20 | "ext-openssl": "*", 21 | "psr/http-message": "^2.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", 25 | "squizlabs/php_codesniffer": "^2.3", 26 | "guzzlehttp/psr7": "^2.4.5", 27 | "yoast/phpunit-polyfills": "^1.0" 28 | 29 | }, 30 | "autoload": { 31 | "psr-4": { "Aws\\Sns\\": "src/" } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { "Aws\\Sns\\": "tests/" } 35 | }, 36 | "scripts": { 37 | "test": "phpunit" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests 7 | 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Exception/InvalidSnsMessageException.php: -------------------------------------------------------------------------------- 1 | getBody()); 56 | } 57 | 58 | /** 59 | * Creates a Message object from a JSON-decodable string. 60 | * 61 | * @param string $requestBody 62 | * @return Message 63 | */ 64 | public static function fromJsonString($requestBody) 65 | { 66 | $data = json_decode($requestBody, true); 67 | if (JSON_ERROR_NONE !== json_last_error() || !is_array($data)) { 68 | throw new \RuntimeException('Invalid POST data.'); 69 | } 70 | 71 | return new Message($data); 72 | } 73 | 74 | /** 75 | * Creates a Message object from an array of raw message data. 76 | * 77 | * @param array $data The message data. 78 | * 79 | * @throws \InvalidArgumentException If a valid type is not provided or 80 | * there are other required keys missing. 81 | */ 82 | public function __construct(array $data) 83 | { 84 | // Ensure that all the required keys for the message's type are present. 85 | $this->validateRequiredKeys($data, self::$requiredKeys); 86 | if ($data['Type'] === 'SubscriptionConfirmation' 87 | || $data['Type'] === 'UnsubscribeConfirmation' 88 | ) { 89 | $this->validateRequiredKeys($data, self::$subscribeKeys); 90 | } 91 | 92 | $this->data = $data; 93 | } 94 | 95 | #[\ReturnTypeWillChange] 96 | public function getIterator() 97 | { 98 | return new \ArrayIterator($this->data); 99 | } 100 | 101 | #[\ReturnTypeWillChange] 102 | public function offsetExists($key) 103 | { 104 | return isset($this->data[$key]); 105 | } 106 | 107 | #[\ReturnTypeWillChange] 108 | public function offsetGet($key) 109 | { 110 | return isset($this->data[$key]) ? $this->data[$key] : null; 111 | } 112 | 113 | #[\ReturnTypeWillChange] 114 | public function offsetSet($key, $value) 115 | { 116 | $this->data[$key] = $value; 117 | } 118 | 119 | #[\ReturnTypeWillChange] 120 | public function offsetUnset($key) 121 | { 122 | unset($this->data[$key]); 123 | } 124 | 125 | /** 126 | * Get all the message data as a plain array. 127 | * 128 | * @return array 129 | */ 130 | public function toArray() 131 | { 132 | return $this->data; 133 | } 134 | 135 | private function validateRequiredKeys(array $data, array $keys) 136 | { 137 | foreach ($keys as $key) { 138 | $keyIsArray = is_array($key); 139 | if (!$keyIsArray) { 140 | $found = isset($data[$key]); 141 | } else { 142 | $found = false; 143 | foreach ($key as $keyOption) { 144 | if (isset($data[$keyOption])) { 145 | $found = true; 146 | break; 147 | } 148 | } 149 | } 150 | 151 | if (!$found) { 152 | if ($keyIsArray) { 153 | $key = $key[0]; 154 | } 155 | throw new \InvalidArgumentException( 156 | "\"{$key}\" is required to verify the SNS Message." 157 | ); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/MessageValidator.php: -------------------------------------------------------------------------------- 1 | .amazonaws.com (AWS) 25 | * - sns.us-gov-west-1.amazonaws.com (AWS GovCloud) 26 | * - sns.cn-north-1.amazonaws.com.cn (AWS China) 27 | */ 28 | private static $defaultHostPattern 29 | = '/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/'; 30 | 31 | private static function isLambdaStyle(Message $message) 32 | { 33 | return isset($message['SigningCertUrl']); 34 | } 35 | 36 | private static function convertLambdaMessage(Message $lambdaMessage) 37 | { 38 | $keyReplacements = [ 39 | 'SigningCertUrl' => 'SigningCertURL', 40 | 'SubscribeUrl' => 'SubscribeURL', 41 | 'UnsubscribeUrl' => 'UnsubscribeURL', 42 | ]; 43 | 44 | $message = clone $lambdaMessage; 45 | foreach ($keyReplacements as $lambdaKey => $canonicalKey) { 46 | if (isset($message[$lambdaKey])) { 47 | $message[$canonicalKey] = $message[$lambdaKey]; 48 | unset($message[$lambdaKey]); 49 | } 50 | } 51 | 52 | return $message; 53 | } 54 | 55 | /** 56 | * Constructs the Message Validator object and ensures that openssl is 57 | * installed. 58 | * 59 | * @param callable $certClient Callable used to download the certificate. 60 | * Should have the following function signature: 61 | * `function (string $certUrl) : string|false $certContent` 62 | * @param string $hostNamePattern 63 | */ 64 | public function __construct( 65 | ?callable $certClient = null, 66 | $hostNamePattern = '' 67 | ) { 68 | $this->certClient = $certClient ?: function($certUrl) { 69 | return @ file_get_contents($certUrl); 70 | }; 71 | $this->hostPattern = $hostNamePattern ?: self::$defaultHostPattern; 72 | } 73 | 74 | /** 75 | * Validates a message from SNS to ensure that it was delivered by AWS. 76 | * 77 | * @param Message $message Message to validate. 78 | * 79 | * @throws InvalidSnsMessageException If the cert cannot be retrieved or its 80 | * source verified, or the message 81 | * signature is invalid. 82 | */ 83 | public function validate(Message $message) 84 | { 85 | if (self::isLambdaStyle($message)) { 86 | $message = self::convertLambdaMessage($message); 87 | } 88 | 89 | // Get the certificate. 90 | $this->validateUrl($message['SigningCertURL']); 91 | $certificate = call_user_func($this->certClient, $message['SigningCertURL']); 92 | if ($certificate === false) { 93 | throw new InvalidSnsMessageException( 94 | "Cannot get the certificate from \"{$message['SigningCertURL']}\"." 95 | ); 96 | } 97 | 98 | // Extract the public key. 99 | $key = openssl_get_publickey($certificate); 100 | if (!$key) { 101 | throw new InvalidSnsMessageException( 102 | 'Cannot get the public key from the certificate.' 103 | ); 104 | } 105 | 106 | // Verify the signature of the message. 107 | $content = $this->getStringToSign($message); 108 | $signature = base64_decode($message['Signature']); 109 | $algo = ($message['SignatureVersion'] === self::SIGNATURE_VERSION_1 ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256); 110 | if (openssl_verify($content, $signature, $key, $algo) !== 1) { 111 | throw new InvalidSnsMessageException( 112 | 'The message signature is invalid.' 113 | ); 114 | } 115 | } 116 | 117 | /** 118 | * Determines if a message is valid and that is was delivered by AWS. This 119 | * method does not throw exceptions and returns a simple boolean value. 120 | * 121 | * @param Message $message The message to validate 122 | * 123 | * @return bool 124 | */ 125 | public function isValid(Message $message) 126 | { 127 | try { 128 | $this->validate($message); 129 | return true; 130 | } catch (InvalidSnsMessageException $e) { 131 | return false; 132 | } 133 | } 134 | 135 | /** 136 | * Builds string-to-sign according to the SNS message spec. 137 | * 138 | * @param Message $message Message for which to build the string-to-sign. 139 | * 140 | * @return string 141 | * @link http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html 142 | */ 143 | public function getStringToSign(Message $message) 144 | { 145 | static $signableKeys = [ 146 | 'Message', 147 | 'MessageId', 148 | 'Subject', 149 | 'SubscribeURL', 150 | 'Timestamp', 151 | 'Token', 152 | 'TopicArn', 153 | 'Type', 154 | ]; 155 | 156 | if ($message['SignatureVersion'] !== self::SIGNATURE_VERSION_1 157 | && $message['SignatureVersion'] !== self::SIGNATURE_VERSION_2) { 158 | throw new InvalidSnsMessageException( 159 | "The SignatureVersion \"{$message['SignatureVersion']}\" is not supported." 160 | ); 161 | } 162 | 163 | $stringToSign = ''; 164 | foreach ($signableKeys as $key) { 165 | if (isset($message[$key])) { 166 | $stringToSign .= "{$key}\n{$message[$key]}\n"; 167 | } 168 | } 169 | 170 | return $stringToSign; 171 | } 172 | 173 | /** 174 | * Ensures that the URL of the certificate is one belonging to AWS, and not 175 | * just something from the amazonaws domain, which could include S3 buckets. 176 | * 177 | * @param string $url Certificate URL 178 | * 179 | * @throws InvalidSnsMessageException if the cert url is invalid. 180 | */ 181 | private function validateUrl($url) 182 | { 183 | $parsed = parse_url($url); 184 | if (empty($parsed['scheme']) 185 | || empty($parsed['host']) 186 | || $parsed['scheme'] !== 'https' 187 | || substr($url, -4) !== '.pem' 188 | || !preg_match($this->hostPattern, $parsed['host']) 189 | ) { 190 | throw new InvalidSnsMessageException( 191 | 'The certificate is located on an invalid domain.' 192 | ); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tests/FunctionalValidationsTest.php: -------------------------------------------------------------------------------- 1 | "Notification", 55 | 'MessageId' => "792cda85-518f-5dd3-9163-81d851212f3a", 56 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-892f85fe-4836-424d-8188-ab85bef0f362", 57 | 'Message' => "Hello world", 58 | 'Timestamp' => "2022-07-28T21:23:58.317Z", 59 | 'SignatureVersion' => "1", 60 | 'Signature' => "ghtf+deOBAzHJJZ6s6CdRLfTQAlcGzq9naoFM1wi0CJiq//uVRuZnamrkWNF0fhouMFvuLVRwcz8PZLUMSfnmd5VpdTKpTyiKmy1qJAZXma0w+yi7G+I33hD1Jyk1Nbym2n0kqp3fVu2aoooiN2ZeLAT2bH0/BtjLSfN1yAOKNoprco4qV9gGUZinXJdj9a1YdNhDR2jKi33ldlsVtEXAtiaDklGEk7DgRKX38GerBPiLg3FdtgY6KC7cdeGpU/dGK+4hjc83Ive1HoFkAwqhpgInM2sMytBosoiXfCmOKmU4xeGD0gHDNZTlJUJQDlzw8Eag0H9f/5zXF9d3uy0YQ==", 61 | 'SigningCertURL' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 62 | 'UnsubscribeURL' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-892f85fe-4836-424d-8188-ab85bef0f362:2296bc94-7992-4be1-b15f-b97229b5c1d8", 63 | ] 64 | ], 65 | [ 66 | [ 67 | 'Type' => "Notification", 68 | 'MessageId' => "17dea24b-55c2-540b-8362-f916557af765", 69 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-62674b1d-4295-426b-88e7-5fb75652a04e", 70 | 'Message' => "Hello world", 71 | 'Timestamp' => "2022-07-28T21:24:08.324Z", 72 | 'SignatureVersion' => "2", 73 | 'Signature' => "CXVqp9PfZAL+4JHS3Zxo1PFbQsvnOjvmYhtIf17TWpwc+iIVas8kZ8GopuzVzVMdatE7rCl/O4P91Zp05Dwz8lk8dLhfp8gSu3Njlzxlyrmzo9x3va3Jb7zFnedgS2GKnZWHGBdwTXho+TosNUE+3e10OMSlwN5XGDwX7+R3WL+rn+AXmFAqp3alg27sYa55h1dLE9cGszGPjScPdtF3BmZsUDMx9wSdNKsCk+vSvE8yBjnCmUl7laSFj3LzPVrlSwgNYCF3kYnNAkah7NplK4SFhJYLwS0HCVCQJKa8rVbQLf9cBTu60U402mrgy0bN8xWoyimzbYbrOMJjalqkUg==", 74 | 'SigningCertURL' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 75 | 'UnsubscribeURL' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-62674b1d-4295-426b-88e7-5fb75652a04e:ad7d16e3-0a7c-46aa-b23e-ffaf02250cbe", 76 | ] 77 | ], 78 | [ 79 | [ 80 | 'Type' => "Notification", 81 | 'MessageId' => "11405cc3-9ac7-56d5-b45d-079e8f7a8edf", 82 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-6e11fed2-fcdf-4c52-9dc4-36ef43f37f84", 83 | 'Subject' => "Hello world", 84 | 'Message' => "Hello world", 85 | 'Timestamp' => "2022-07-28T22:53:49.654Z", 86 | 'SignatureVersion' => "1", 87 | 'Signature' => "AItkS26d8yvnIKJevdirIPW7eM/yKbZy3/CF2EreCHmXWB3etWaV5Fb7SYpGABMpugpDZzNyGY1wCVWaopDoQ+7Q/kI2TpDu8bw1eExbi8U3kduvc/2m2fIrI4gDEY8/v3nzoLcr8pPodqMzrX6SzQou4klfaqbNK+rFmH0LVf2Q1VyOROODoSXmo4jg2Yu12jfxccBl96Drr/ihq4MJ4OcrWh6UzXXlVYjJHx2Ui4anNwNEb+Z4C2CAF1DjQUbhDtaoajDBPY+4d9C1OwbqwQpXsd6tyVcI9nFyEsVK8lfnAV+/3GZQcdXHbIUYBRGcBa4X5TlWJku5nDH2ERtHHw==", 88 | 'SigningCertURL' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 89 | 'UnsubscribeURL' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-6e11fed2-fcdf-4c52-9dc4-36ef43f37f84:adb318c3-2234-4c56-905d-c324cf0df874", 90 | ] 91 | ], 92 | [ 93 | [ 94 | 'Type' => "Notification", 95 | 'MessageId' => "4504e649-d933-5aa9-8199-bd14ccf05f0b", 96 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-530b26da-0687-4fe4-9f71-780bad3181e2", 97 | 'Subject' => "Hello world", 98 | 'Message' => "Hello world", 99 | 'Timestamp' => "2022-07-28T22:53:55.086Z", 100 | 'SignatureVersion' => "2", 101 | 'Signature' => "cETcSvmmkt+My05qCLKexyl0+RyG83mSryKPqTfS+tYcxDJWVcjPJAr+qdpElzVaBl1aTGYVWMY64i9JqZ/JES8pylNj8LGvdhuNQKO59/WCoIimZAsNhn0xEgOeeDU+W/0BU4sdpCGMNjo0S/FuIiWaRe4E0YWRVrxeQevaQ70euDdfWgd5v1eCKQz8b367b9XBmMztL/CWUFI6YaKK/MV21eyvJe3Y7CtVYiOKEYiAZnAEkynK7gUGO5TsgDjGNYhj6U3xYsWgI03bmioSl7kdFSUj+AZ7ugas5fghqxgoDsdfqsjMYKRm5KKHQWsgzI619yIzpNKUiSMHxdZXpQ==", 102 | 'SigningCertURL' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 103 | 'UnsubscribeURL' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-530b26da-0687-4fe4-9f71-780bad3181e2:db0ad2ad-03d1-48ca-a5da-51f317800a57" 104 | ] 105 | ] 106 | ]; 107 | } 108 | 109 | public function getLambdaFixtures() 110 | { 111 | return [ 112 | [ 113 | [ 114 | 'Type' => "Notification", 115 | 'MessageId' => "792cda85-518f-5dd3-9163-81d851212f3a", 116 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-892f85fe-4836-424d-8188-ab85bef0f362", 117 | 'Message' => "Hello world", 118 | 'Timestamp' => "2022-07-28T21:23:58.317Z", 119 | 'SignatureVersion' => "1", 120 | 'Signature' => "ghtf+deOBAzHJJZ6s6CdRLfTQAlcGzq9naoFM1wi0CJiq//uVRuZnamrkWNF0fhouMFvuLVRwcz8PZLUMSfnmd5VpdTKpTyiKmy1qJAZXma0w+yi7G+I33hD1Jyk1Nbym2n0kqp3fVu2aoooiN2ZeLAT2bH0/BtjLSfN1yAOKNoprco4qV9gGUZinXJdj9a1YdNhDR2jKi33ldlsVtEXAtiaDklGEk7DgRKX38GerBPiLg3FdtgY6KC7cdeGpU/dGK+4hjc83Ive1HoFkAwqhpgInM2sMytBosoiXfCmOKmU4xeGD0gHDNZTlJUJQDlzw8Eag0H9f/5zXF9d3uy0YQ==", 121 | 'SigningCertUrl' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 122 | 'UnsubscribeUrl' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-892f85fe-4836-424d-8188-ab85bef0f362:2296bc94-7992-4be1-b15f-b97229b5c1d8", 123 | ] 124 | ], 125 | [ 126 | [ 127 | 'Type' => "Notification", 128 | 'MessageId' => "17dea24b-55c2-540b-8362-f916557af765", 129 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-62674b1d-4295-426b-88e7-5fb75652a04e", 130 | 'Message' => "Hello world", 131 | 'Timestamp' => "2022-07-28T21:24:08.324Z", 132 | 'SignatureVersion' => "2", 133 | 'Signature' => "CXVqp9PfZAL+4JHS3Zxo1PFbQsvnOjvmYhtIf17TWpwc+iIVas8kZ8GopuzVzVMdatE7rCl/O4P91Zp05Dwz8lk8dLhfp8gSu3Njlzxlyrmzo9x3va3Jb7zFnedgS2GKnZWHGBdwTXho+TosNUE+3e10OMSlwN5XGDwX7+R3WL+rn+AXmFAqp3alg27sYa55h1dLE9cGszGPjScPdtF3BmZsUDMx9wSdNKsCk+vSvE8yBjnCmUl7laSFj3LzPVrlSwgNYCF3kYnNAkah7NplK4SFhJYLwS0HCVCQJKa8rVbQLf9cBTu60U402mrgy0bN8xWoyimzbYbrOMJjalqkUg==", 134 | 'SigningCertUrl' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 135 | 'UnsubscribeUrl' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-62674b1d-4295-426b-88e7-5fb75652a04e:ad7d16e3-0a7c-46aa-b23e-ffaf02250cbe", 136 | ] 137 | ], 138 | [ 139 | [ 140 | 'Type' => "Notification", 141 | 'MessageId' => "792cda85-518f-5dd3-9163-81d851212f3a", 142 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-892f85fe-4836-424d-8188-ab85bef0f362", 143 | 'Subject' => null, 144 | 'Message' => "Hello world", 145 | 'Timestamp' => "2022-07-28T21:23:58.317Z", 146 | 'SignatureVersion' => "1", 147 | 'Signature' => "ghtf+deOBAzHJJZ6s6CdRLfTQAlcGzq9naoFM1wi0CJiq//uVRuZnamrkWNF0fhouMFvuLVRwcz8PZLUMSfnmd5VpdTKpTyiKmy1qJAZXma0w+yi7G+I33hD1Jyk1Nbym2n0kqp3fVu2aoooiN2ZeLAT2bH0/BtjLSfN1yAOKNoprco4qV9gGUZinXJdj9a1YdNhDR2jKi33ldlsVtEXAtiaDklGEk7DgRKX38GerBPiLg3FdtgY6KC7cdeGpU/dGK+4hjc83Ive1HoFkAwqhpgInM2sMytBosoiXfCmOKmU4xeGD0gHDNZTlJUJQDlzw8Eag0H9f/5zXF9d3uy0YQ==", 148 | 'SigningCertUrl' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 149 | 'UnsubscribeUrl' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-892f85fe-4836-424d-8188-ab85bef0f362:2296bc94-7992-4be1-b15f-b97229b5c1d8", 150 | ] 151 | ], 152 | [ 153 | [ 154 | 'Type' => "Notification", 155 | 'MessageId' => "17dea24b-55c2-540b-8362-f916557af765", 156 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-62674b1d-4295-426b-88e7-5fb75652a04e", 157 | 'Subject' => null, 158 | 'Message' => "Hello world", 159 | 'Timestamp' => "2022-07-28T21:24:08.324Z", 160 | 'SignatureVersion' => "2", 161 | 'Signature' => "CXVqp9PfZAL+4JHS3Zxo1PFbQsvnOjvmYhtIf17TWpwc+iIVas8kZ8GopuzVzVMdatE7rCl/O4P91Zp05Dwz8lk8dLhfp8gSu3Njlzxlyrmzo9x3va3Jb7zFnedgS2GKnZWHGBdwTXho+TosNUE+3e10OMSlwN5XGDwX7+R3WL+rn+AXmFAqp3alg27sYa55h1dLE9cGszGPjScPdtF3BmZsUDMx9wSdNKsCk+vSvE8yBjnCmUl7laSFj3LzPVrlSwgNYCF3kYnNAkah7NplK4SFhJYLwS0HCVCQJKa8rVbQLf9cBTu60U402mrgy0bN8xWoyimzbYbrOMJjalqkUg==", 162 | 'SigningCertUrl' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 163 | 'UnsubscribeUrl' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-62674b1d-4295-426b-88e7-5fb75652a04e:ad7d16e3-0a7c-46aa-b23e-ffaf02250cbe", 164 | ] 165 | ], 166 | [ 167 | [ 168 | 'Type' => "Notification", 169 | 'MessageId' => "11405cc3-9ac7-56d5-b45d-079e8f7a8edf", 170 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-6e11fed2-fcdf-4c52-9dc4-36ef43f37f84", 171 | 'Subject' => "Hello world", 172 | 'Message' => "Hello world", 173 | 'Timestamp' => "2022-07-28T22:53:49.654Z", 174 | 'SignatureVersion' => "1", 175 | 'Signature' => "AItkS26d8yvnIKJevdirIPW7eM/yKbZy3/CF2EreCHmXWB3etWaV5Fb7SYpGABMpugpDZzNyGY1wCVWaopDoQ+7Q/kI2TpDu8bw1eExbi8U3kduvc/2m2fIrI4gDEY8/v3nzoLcr8pPodqMzrX6SzQou4klfaqbNK+rFmH0LVf2Q1VyOROODoSXmo4jg2Yu12jfxccBl96Drr/ihq4MJ4OcrWh6UzXXlVYjJHx2Ui4anNwNEb+Z4C2CAF1DjQUbhDtaoajDBPY+4d9C1OwbqwQpXsd6tyVcI9nFyEsVK8lfnAV+/3GZQcdXHbIUYBRGcBa4X5TlWJku5nDH2ERtHHw==", 176 | 'SigningCertUrl' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 177 | 'UnsubscribeUrl' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-6e11fed2-fcdf-4c52-9dc4-36ef43f37f84:adb318c3-2234-4c56-905d-c324cf0df874", 178 | ] 179 | ], 180 | [ 181 | [ 182 | 'Type' => "Notification", 183 | 'MessageId' => "4504e649-d933-5aa9-8199-bd14ccf05f0b", 184 | 'TopicArn' => "arn:aws:sns:us-east-2:295079676684:publish-and-verify-530b26da-0687-4fe4-9f71-780bad3181e2", 185 | 'Subject' => "Hello world", 186 | 'Message' => "Hello world", 187 | 'Timestamp' => "2022-07-28T22:53:55.086Z", 188 | 'SignatureVersion' => "2", 189 | 'Signature' => "cETcSvmmkt+My05qCLKexyl0+RyG83mSryKPqTfS+tYcxDJWVcjPJAr+qdpElzVaBl1aTGYVWMY64i9JqZ/JES8pylNj8LGvdhuNQKO59/WCoIimZAsNhn0xEgOeeDU+W/0BU4sdpCGMNjo0S/FuIiWaRe4E0YWRVrxeQevaQ70euDdfWgd5v1eCKQz8b367b9XBmMztL/CWUFI6YaKK/MV21eyvJe3Y7CtVYiOKEYiAZnAEkynK7gUGO5TsgDjGNYhj6U3xYsWgI03bmioSl7kdFSUj+AZ7ugas5fghqxgoDsdfqsjMYKRm5KKHQWsgzI619yIzpNKUiSMHxdZXpQ==", 190 | 'SigningCertUrl' => "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", 191 | 'UnsubscribeUrl' => "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:295079676684:publish-and-verify-530b26da-0687-4fe4-9f71-780bad3181e2:db0ad2ad-03d1-48ca-a5da-51f317800a57" 192 | ] 193 | ] 194 | ]; 195 | } 196 | 197 | private function getMockCertServerClient() 198 | { 199 | return function () { 200 | return self::$certificate; 201 | }; 202 | } 203 | 204 | /** 205 | * @dataProvider getHttpFixtures 206 | * 207 | * @param array $messageData 208 | */ 209 | public function testValidatesHttpFixtures($messageData) 210 | { 211 | $validator = new MessageValidator($this->getMockCertServerClient()); 212 | $message = new Message($messageData); 213 | 214 | $this->assertTrue($validator->isValid($message)); 215 | $this->assertNotEmpty($message['SigningCertURL']); 216 | } 217 | 218 | /** 219 | * @dataProvider getLambdaFixtures 220 | * 221 | * @param array $messageData 222 | */ 223 | public function testValidatesLambdaFixtures($messageData) 224 | { 225 | $validator = new MessageValidator($this->getMockCertServerClient()); 226 | $message = new Message($messageData); 227 | 228 | $this->assertTrue($validator->isValid($message)); 229 | $this->assertNotEmpty($message['SigningCertUrl']); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /tests/MessageTest.php: -------------------------------------------------------------------------------- 1 | 'a', 14 | 'MessageId' => 'b', 15 | 'Timestamp' => 'c', 16 | 'TopicArn' => 'd', 17 | 'Type' => 'e', 18 | 'Subject' => 'f', 19 | 'Signature' => 'g', 20 | 'SignatureVersion' => '1', 21 | 'SigningCertURL' => 'h', 22 | 'SubscribeURL' => 'i', 23 | 'Token' => 'j', 24 | ); 25 | 26 | public function testGetters() 27 | { 28 | $message = new Message($this->messageData); 29 | $this->assertIsArray($message->toArray()); 30 | 31 | foreach ($this->messageData as $key => $expectedValue) { 32 | $this->assertTrue(isset($message[$key])); 33 | $this->assertEquals($expectedValue, $message[$key]); 34 | } 35 | } 36 | 37 | public function testIterable() 38 | { 39 | $message = new Message($this->messageData); 40 | 41 | $this->assertInstanceOf('Traversable', $message); 42 | foreach ($message as $key => $value) { 43 | $this->assertTrue(isset($this->messageData[$key])); 44 | $this->assertEquals($value, $this->messageData[$key]); 45 | } 46 | } 47 | 48 | /** 49 | * @dataProvider messageTypeProvider 50 | * 51 | * @param string $messageType 52 | */ 53 | public function testConstructorSucceedsWithGoodData($messageType) 54 | { 55 | $this->assertInstanceOf('Aws\Sns\Message', new Message( 56 | ['Type' => $messageType] + $this->messageData 57 | )); 58 | } 59 | 60 | public function messageTypeProvider() 61 | { 62 | return [ 63 | ['Notification'], 64 | ['SubscriptionConfirmation'], 65 | ['UnsubscribeConfirmation'], 66 | ]; 67 | } 68 | 69 | public function testConstructorFailsWithNoType() 70 | { 71 | $this->expectException(\InvalidArgumentException::class); 72 | $data = $this->messageData; 73 | unset($data['Type']); 74 | new Message($data); 75 | } 76 | 77 | public function testConstructorFailsWithMissingData() 78 | { 79 | $this->expectException(\InvalidArgumentException::class); 80 | new Message(['Type' => 'Notification']); 81 | } 82 | 83 | public function testRequiresTokenAndSubscribeUrlForSubscribeMessage() 84 | { 85 | $this->expectException(\InvalidArgumentException::class); 86 | new Message( 87 | ['Type' => 'SubscriptionConfirmation'] + array_diff_key( 88 | $this->messageData, 89 | array_flip(['Token', 'SubscribeURL']) 90 | ) 91 | ); 92 | } 93 | 94 | public function testRequiresTokenAndSubscribeUrlForUnsubscribeMessage() 95 | { 96 | $this->expectException(\InvalidArgumentException::class); 97 | new Message( 98 | ['Type' => 'UnsubscribeConfirmation'] + array_diff_key( 99 | $this->messageData, 100 | array_flip(['Token', 'SubscribeURL']) 101 | ) 102 | ); 103 | } 104 | 105 | public function testCanCreateFromRawPost() 106 | { 107 | $_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE'] = 'Notification'; 108 | 109 | // Prep php://input with mocked data 110 | MockPhpStream::setStartingData(json_encode($this->messageData)); 111 | stream_wrapper_unregister('php'); 112 | stream_wrapper_register('php', __NAMESPACE__ . '\MockPhpStream'); 113 | 114 | $message = Message::fromRawPostData(); 115 | $this->assertInstanceOf('Aws\Sns\Message', $message); 116 | 117 | stream_wrapper_restore("php"); 118 | unset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE']); 119 | } 120 | 121 | public function testCreateFromRawPostFailsWithMissingHeader() 122 | { 123 | $this->expectException(\RuntimeException::class); 124 | Message::fromRawPostData(); 125 | } 126 | 127 | public function testCreateFromRawPostFailsWithMissingData() 128 | { 129 | $this->expectException(\RuntimeException::class); 130 | $_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE'] = 'Notification'; 131 | Message::fromRawPostData(); 132 | unset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE']); 133 | } 134 | 135 | public function testCanCreateFromPsr7Request() 136 | { 137 | $request = new Request( 138 | 'POST', 139 | '/', 140 | [], 141 | json_encode($this->messageData) 142 | ); 143 | $message = Message::fromPsrRequest($request); 144 | $this->assertInstanceOf('Aws\Sns\Message', $message); 145 | } 146 | 147 | public function testCreateFromPsr7RequestFailsWithMissingData() 148 | { 149 | $this->expectException(\RuntimeException::class); 150 | $request = new Request( 151 | 'POST', 152 | '/', 153 | [], 154 | 'Not valid JSON' 155 | ); 156 | Message::fromPsrRequest($request); 157 | } 158 | 159 | public function testArrayAccess() 160 | { 161 | $message = new Message($this->messageData); 162 | 163 | $this->assertInstanceOf('ArrayAccess', $message); 164 | $message['foo'] = 'bar'; 165 | $this->assertTrue(isset($message['foo'])); 166 | $this->assertTrue($message['foo'] === 'bar'); 167 | unset($message['foo']); 168 | $this->assertFalse(isset($message['foo'])); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/MessageValidatorTest.php: -------------------------------------------------------------------------------- 1 | getMockHttpClient()); 34 | $message = $this->getTestMessage([ 35 | 'SignatureVersion' => '2', 36 | ]); 37 | $this->assertFalse($validator->isValid($message)); 38 | } 39 | 40 | public function testValidateFailsWhenSignatureVersionIsInvalid() 41 | { 42 | $this->expectException(InvalidSnsMessageException::class); 43 | $this->expectExceptionMessage('The SignatureVersion "3" is not supported.'); 44 | $validator = new MessageValidator($this->getMockCertServerClient()); 45 | $message = $this->getTestMessage([ 46 | 'SignatureVersion' => '3', 47 | ]); 48 | $validator->validate($message); 49 | } 50 | 51 | public function testValidateFailsWhenCertUrlInvalid() 52 | { 53 | $this->expectException(InvalidSnsMessageException::class); 54 | $this->expectExceptionMessage('The certificate is located on an invalid domain.'); 55 | $validator = new MessageValidator(); 56 | $message = $this->getTestMessage([ 57 | 'SigningCertURL' => 'https://foo.amazonaws.com/bar.pem', 58 | ]); 59 | $validator->validate($message); 60 | } 61 | 62 | public function testValidateFailsWhenCertUrlNotAPemFile() 63 | { 64 | $this->expectException(InvalidSnsMessageException::class); 65 | $this->expectExceptionMessage('The certificate is located on an invalid domain.'); 66 | $validator = new MessageValidator(); 67 | $message = $this->getTestMessage([ 68 | 'SigningCertURL' => 'https://foo.amazonaws.com/bar', 69 | ]); 70 | $validator->validate($message); 71 | } 72 | 73 | public function testValidatesAgainstCustomDomains() 74 | { 75 | $validator = new MessageValidator( 76 | function () { 77 | return self::$certificate; 78 | }, 79 | '/^(foo|bar).example.com$/' 80 | ); 81 | $message = $this->getTestMessage([ 82 | 'SigningCertURL' => 'https://foo.example.com/baz.pem', 83 | ]); 84 | $message['Signature'] = $this->getSignature($validator->getStringToSign($message)); 85 | $this->assertTrue($validator->isValid($message)); 86 | } 87 | 88 | public function testValidateFailsWhenCannotGetCertificate() 89 | { 90 | $this->expectException(InvalidSnsMessageException::class); 91 | $this->expectDeprecationMessageMatches('/Cannot get the certificate from ".+"./'); 92 | $validator = new MessageValidator($this->getMockHttpClient(false)); 93 | $message = $this->getTestMessage(); 94 | $validator->validate($message); 95 | } 96 | 97 | public function testValidateFailsWhenCannotDeterminePublicKey() 98 | { 99 | $this->expectException(InvalidSnsMessageException::class); 100 | $this->expectExceptionMessage('Cannot get the public key from the certificate.'); 101 | $validator = new MessageValidator($this->getMockHttpClient()); 102 | $message = $this->getTestMessage(); 103 | $validator->validate($message); 104 | } 105 | 106 | public function testValidateFailsWhenMessageIsInvalid() 107 | { 108 | $this->expectException(InvalidSnsMessageException::class); 109 | $this->expectExceptionMessage('The message signature is invalid.'); 110 | $validator = new MessageValidator($this->getMockCertServerClient()); 111 | $message = $this->getTestMessage([ 112 | 'Signature' => $this->getSignature('foo'), 113 | ]); 114 | $validator->validate($message); 115 | } 116 | 117 | public function testValidateFailsWhenSha256MessageIsInvalid() 118 | { 119 | $this->expectException(InvalidSnsMessageException::class); 120 | $this->expectExceptionMessage('The message signature is invalid.'); 121 | $validator = new MessageValidator($this->getMockCertServerClient()); 122 | $message = $this->getTestMessage([ 123 | 'Signature' => $this->getSignature('foo'), 124 | 'SignatureVersion' => '2' 125 | 126 | ]); 127 | $validator->validate($message); 128 | } 129 | 130 | public function testValidateSucceedsWhenMessageIsValid() 131 | { 132 | $validator = new MessageValidator($this->getMockCertServerClient()); 133 | $message = $this->getTestMessage(); 134 | 135 | // Get the signature for a real message 136 | $message['Signature'] = $this->getSignature($validator->getStringToSign($message)); 137 | 138 | // The message should validate 139 | $this->assertTrue($validator->isValid($message)); 140 | } 141 | 142 | public function testValidateSucceedsWhenSha256MessageIsValid() 143 | { 144 | $validator = new MessageValidator($this->getMockCertServerClient()); 145 | $message = $this->getTestMessage([ 146 | 'SignatureVersion' => '2' 147 | ]); 148 | 149 | // Get the signature for a real message 150 | $message['Signature'] = $this->getSignature($validator->getStringToSign($message), '2'); 151 | 152 | // The message should validate 153 | $this->assertTrue($validator->isValid($message)); 154 | } 155 | 156 | public function testBuildsStringToSignCorrectly() 157 | { 158 | $validator = new MessageValidator(); 159 | $stringToSign = <<< STRINGTOSIGN 160 | Message 161 | foo 162 | MessageId 163 | bar 164 | Timestamp 165 | 1435697129 166 | TopicArn 167 | baz 168 | Type 169 | Notification 170 | 171 | STRINGTOSIGN; 172 | 173 | $this->assertEquals( 174 | $stringToSign, 175 | $validator->getStringToSign($this->getTestMessage()) 176 | ); 177 | } 178 | 179 | /** 180 | * @param array $customData 181 | * 182 | * @return Message 183 | */ 184 | private function getTestMessage(array $customData = []) 185 | { 186 | return new Message($customData + [ 187 | 'Message' => 'foo', 188 | 'MessageId' => 'bar', 189 | 'Timestamp' => time(), 190 | 'TopicArn' => 'baz', 191 | 'Type' => 'Notification', 192 | 'SigningCertURL' => self::VALID_CERT_URL, 193 | 'Signature' => true, 194 | 'SignatureVersion' => '1', 195 | ]); 196 | } 197 | 198 | private function getMockHttpClient($responseBody = '') 199 | { 200 | return function () use ($responseBody) { 201 | return $responseBody; 202 | }; 203 | } 204 | 205 | private function getMockCertServerClient() 206 | { 207 | return function ($url) { 208 | if ($url !== self::VALID_CERT_URL) { 209 | return ''; 210 | } 211 | 212 | return self::$certificate; 213 | }; 214 | } 215 | 216 | private function getSignature($stringToSign, $algo = '1') 217 | { 218 | if ($algo === '2') { 219 | openssl_sign($stringToSign, $signature, self::$pKey, 'SHA256'); 220 | } else { 221 | openssl_sign($stringToSign, $signature, self::$pKey); 222 | } 223 | 224 | return base64_encode($signature); 225 | } 226 | } 227 | 228 | function time() 229 | { 230 | return 1435697129; 231 | } 232 | -------------------------------------------------------------------------------- /tests/MockPhpStream.php: -------------------------------------------------------------------------------- 1 | data = self::$startingData; 19 | $this->index = 0; 20 | $this->length = strlen(self::$startingData); 21 | } 22 | 23 | public function stream_open($path, $mode, $options, &$opened_path) 24 | { 25 | return true; 26 | } 27 | 28 | public function stream_close() 29 | { 30 | } 31 | 32 | public function stream_stat() 33 | { 34 | return array(); 35 | } 36 | 37 | public function stream_flush() 38 | { 39 | return true; 40 | } 41 | 42 | public function stream_read($count) 43 | { 44 | $length = min($count, $this->length - $this->index); 45 | $data = substr($this->data, $this->index); 46 | $this->index = $this->index + $length; 47 | 48 | return $data; 49 | } 50 | 51 | public function stream_eof() 52 | { 53 | return ($this->index >= $this->length); 54 | } 55 | 56 | public function stream_write($data) 57 | { 58 | return 0; 59 | } 60 | } 61 | --------------------------------------------------------------------------------