├── .codescanignore
├── .github
└── workflows
│ └── php.yml
├── .gitignore
├── .gitmodules
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── composer.json
├── composer.lock
├── docker-compose.yaml
├── phpunit.xml
├── repo-info.json
├── src
└── Escher
│ ├── AuthElements.php
│ ├── Escher.php
│ ├── Exception.php
│ ├── Provider.php
│ ├── RequestCanonicalizer.php
│ ├── RequestHelper.php
│ ├── Signer.php
│ └── Utils.php
└── test
└── Escher
└── Test
├── EndToEnd
└── CentralTest.php
├── Helper
├── JsonTestCase.php
└── TestBase.php
└── Unit
├── AuthenticateRequestTest.php
├── InternalTest.php
├── RequestCanonicalizerTest.php
├── SignRequestUsingHeaderTest.php
└── SignRequestUsingQueryStringTest.php
/.codescanignore:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | tags:
7 | - '*'
8 | pull_request:
9 | branches: [ master ]
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
17 |
18 | steps:
19 | - name: Install prerequesits
20 | run: sudo apt update && sudo apt install -y php-mbstring
21 | - uses: actions/checkout@v4
22 | with:
23 | submodules: recursive
24 | - uses: shivammathur/setup-php@v2
25 | with:
26 | php-version: ${{ matrix.php-versions }}
27 | extensions: mbstring
28 | tools: composer
29 | - name: Install dependencies
30 | run: composer update && composer install
31 | - name: Test
32 | run: composer test
33 | - name: Deploy
34 | if: startsWith(github.ref, 'refs/tags') && matrix.php-versions == '8.3'
35 | run: |
36 | curl -XPOST -f -H'content-type:application/json' "https://packagist.org/api/update-package?username=emartech&apiToken=${{secrets.PACKAGIST_API_TOKEN}}" -d"{\"repository\":{\"url\":\"${{secrets.PACKAGIST_PACKAGE_URL}}\"}}"
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .phpunit.result.cache
3 | composer.phar
4 | vendor/
5 | build/
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "test-cases"]
2 | path = test-cases
3 | url = git@github.com:EscherAuth/test-cases.git
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.3-alpine3.15
2 |
3 | ENV COMPOSER_ALLOW_SUPERUSER 1
4 | ENV COMPOSER_HOME /tmp
5 | ENV COMPOSER_VERSION 2.2.23
6 |
7 | RUN set -eux ; \
8 | apk add --no-cache --virtual .composer-rundeps \
9 | bash \
10 | coreutils \
11 | git \
12 | make \
13 | openssh-client \
14 | patch \
15 | subversion \
16 | tini \
17 | bzip2 \
18 | bzip2-dev \
19 | zlib \
20 | zlib-dev \
21 | libzip \
22 | libzip-dev \
23 | unzip \
24 | zip
25 |
26 | RUN set -eux ; \
27 | # install necessary/useful extensions not included in base image
28 | docker-php-ext-install \
29 | bz2 \
30 | zip \
31 | ; \
32 | # download installer.php, see https://getcomposer.org/download/
33 | curl \
34 | --silent \
35 | --fail \
36 | --location \
37 | --retry 3 \
38 | --output /tmp/installer.php \
39 | --url https://raw.githubusercontent.com/composer/getcomposer.org/f24b8f860b95b52167f91bbd3e3a7bcafe043038/web/installer \
40 | ; \
41 | # install composer phar binary
42 | php /tmp/installer.php \
43 | --no-ansi \
44 | --install-dir=/usr/bin \
45 | --filename=composer \
46 | --version=${COMPOSER_VERSION} \
47 | ; \
48 | composer --ansi --version --no-interaction ; \
49 | composer diagnose ; \
50 | rm -f /tmp/installer.php ; \
51 | find /tmp -type d -exec chmod -v 1777 {} + \
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Emarsys Technologies Kft.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test
2 |
3 | build: ; docker compose build
4 |
5 | install: ; docker compose run --rm web composer install
6 | update: ; docker compose run --rm web composer update
7 |
8 | test: ; docker compose run --rm web php -d error_reporting=E_ALL ./vendor/bin/phpunit --do-not-cache-result -c phpunit.xml
9 | test-only: ; docker compose run --rm web ./vendor/bin/phpunit --do-not-cache-result --group only -c phpunit.xml
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | EscherPHP - HTTP request signing lib [](https://travis-ci.org/emartech/escher-php)
2 | ===================================
3 |
4 | Escher helps you creating secure HTTP requests (for APIs) by signing HTTP(s) requests. It's both a server side and client side implementation. The status is work in progress.
5 |
6 | The algorithm is based on [Amazon's _AWS Signature Version 4_](http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html), but we have generalized and extended it.
7 |
8 | More details will be available at our [documentation site](https://documentation.emarsys.com/).
9 |
10 |
11 | Signing a request
12 | -----------------
13 |
14 | Escher works by calculating a cryptographic signature of your request, and adding it (and other authentication information) to said request.
15 | Usually you will want to add the authentication information to the request by appending extra headers to it.
16 | Let's say you want to send a signed POST request to http://example.com/ using the Guzzle\Http library:
17 |
18 | ```php
19 | 'application/json');
27 |
28 | $headersWithAuthInfo = Escher::create('example/credential/scope')
29 | ->signRequest('YOUR_ACCESS_KEY_ID', 'YOUR SECRET', $method, $url, $requestBody, $yourHeaders);
30 |
31 | $client = new \GuzzleHttp\Client();
32 | $response = $client->post($url, array(
33 | 'body' => $requestBody,
34 | 'headers' => $headersWithAuthInfo
35 | ));
36 |
37 | ```
38 |
39 | Presigning an URL
40 | -----------------
41 |
42 | In some cases you may want to send authenticated requests from a context where you cannot modify the request headers, e.g. when embedding an API generated iframe.
43 | You can however generate a presigned URL, where the authentication information is added to the query string.
44 |
45 | ```php
46 | presignUrl('YOUR_ACCESS_KEY_ID', 'YOUR SECRET', 'http://example.com');
52 |
53 | ```
54 |
55 | Validating a request
56 | --------------------
57 |
58 | You can validate a request signed by the methods described above. For that you will need a database of the access keys and secrets of your clients.
59 | Escher accepts any kind of object as a key database that implements the ArrayAccess interface. (It also accepts plain arrays, however it is not recommended to use a php array for a database of API secrets - it's just there to ease testing)
60 |
61 | ```php
62 | 'SECRET OF CLIENT 1',
70 | 'ACCESS_KEY_OF_CLIENT_42' => 'SECRET OF CLIENT 42',
71 | ));
72 | Escher::create('example/credential/scope')->authenticate($keyDB);
73 | } catch (Exception $ex) {
74 | echo 'The validation failed! ' . $ex->getMessage();
75 | }
76 |
77 | ```
78 |
79 | Exceptions
80 | -------------
81 |
82 | | Code pattern | Exception type |
83 | |--------------|-----------------------------|
84 | | 1xxx | Missing exceptions |
85 | | 2xxx | Invalid format exceptions |
86 | | 3xxx | Argument invalid exceptions |
87 | | 4xxx | Not signed exceptions |
88 | | 5xxx | Expired exception |
89 | | 6xxx | Signature exceptions |
90 |
91 | | Code | Message |
92 | |------|------------------------------------------------------------------------------|
93 | | 1001 | The authorization header is missing |
94 | | 1100 | The {PARAM} header is missing |
95 | | 1101 | Query key: {PARAM} is missing |
96 | | 1102 | The host header is missing |
97 | | 2001 | Date header is invalid, the expected format is Wed, 04 Nov 2015 09:20:22 GMT |
98 | | 2002 | Could not parse auth header |
99 | | 2003 | Invalid {PARAM} query key format |
100 | | 2004 | Date header is invalid, the expected format is 20151104T092022Z |
101 | | 3001 | Invalid Escher key |
102 | | 3002 | Only SHA256 and SHA512 hash algorithms are allowed |
103 | | 3003 | The credential scope is invalid |
104 | | 3004 | The credential date does not match with the request date |
105 | | 4001 | The host header is not signed |
106 | | 4002 | The {PARAM} header is not signed |
107 | | 5001 | The request date is not within the accepted time range |
108 | | 6001 | The signatures do not match |
109 |
110 | Configuration
111 | -------------
112 |
113 | TBA
114 |
115 | Running tests
116 | -------------
117 | 1. Install packages with Composer: `composer install`
118 | 2. Run tests with `make tests`
119 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "emartech/escher",
3 | "description": "Library for HTTP request signing (PHP implementation)",
4 | "homepage": "https://github.com/emartech/escher-php",
5 | "authors": [
6 | {
7 | "name": "Emarsys Technologies",
8 | "homepage": "http://emarsys.com/",
9 | "role": "company"
10 | },
11 | {
12 | "name": "Andras Barthazi",
13 | "email": "andras@barthazi.hu",
14 | "homepage": "https://github.com/boogie/",
15 | "role": "lead"
16 | },
17 | {
18 | "name": "Csaba Peter Simon",
19 | "email": "fqqdk1@gmail.com",
20 | "homepage": "https://github.com/fqqdk/",
21 | "role": "developer"
22 | }
23 | ],
24 | "support": {
25 | "issues": "https://github.com/emartech/escher-php/issues"
26 | },
27 | "license": [
28 | "MIT"
29 | ],
30 | "type": "library",
31 | "autoload": {
32 | "psr-4": {"Escher\\": "src/Escher"}
33 | },
34 | "autoload-dev": {
35 | "psr-4": {"Escher\\Test\\": "test/Escher/Test"}
36 | },
37 | "require": {
38 | "ext-json": "*",
39 | "php": ">=7.3"
40 | },
41 | "require-dev": {
42 | "phpunit/phpunit": "9.6.19"
43 | },
44 | "keywords": [
45 | "escher",
46 | "hmac",
47 | "sha",
48 | "aws",
49 | "signature",
50 | "http",
51 | "request",
52 | "rest",
53 | "authentication",
54 | "api"
55 | ],
56 | "scripts": {
57 | "test": "vendor/bin/phpunit -c phpunit.xml"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "76e2a2bd557930e0f1f9829479e0ae9a",
8 | "packages": [],
9 | "packages-dev": [
10 | {
11 | "name": "doctrine/instantiator",
12 | "version": "1.5.0",
13 | "source": {
14 | "type": "git",
15 | "url": "https://github.com/doctrine/instantiator.git",
16 | "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
17 | },
18 | "dist": {
19 | "type": "zip",
20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
21 | "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
22 | "shasum": ""
23 | },
24 | "require": {
25 | "php": "^7.1 || ^8.0"
26 | },
27 | "require-dev": {
28 | "doctrine/coding-standard": "^9 || ^11",
29 | "ext-pdo": "*",
30 | "ext-phar": "*",
31 | "phpbench/phpbench": "^0.16 || ^1",
32 | "phpstan/phpstan": "^1.4",
33 | "phpstan/phpstan-phpunit": "^1",
34 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
35 | "vimeo/psalm": "^4.30 || ^5.4"
36 | },
37 | "type": "library",
38 | "autoload": {
39 | "psr-4": {
40 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
41 | }
42 | },
43 | "notification-url": "https://packagist.org/downloads/",
44 | "license": [
45 | "MIT"
46 | ],
47 | "authors": [
48 | {
49 | "name": "Marco Pivetta",
50 | "email": "ocramius@gmail.com",
51 | "homepage": "https://ocramius.github.io/"
52 | }
53 | ],
54 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
55 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
56 | "keywords": [
57 | "constructor",
58 | "instantiate"
59 | ],
60 | "support": {
61 | "issues": "https://github.com/doctrine/instantiator/issues",
62 | "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
63 | },
64 | "funding": [
65 | {
66 | "url": "https://www.doctrine-project.org/sponsorship.html",
67 | "type": "custom"
68 | },
69 | {
70 | "url": "https://www.patreon.com/phpdoctrine",
71 | "type": "patreon"
72 | },
73 | {
74 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
75 | "type": "tidelift"
76 | }
77 | ],
78 | "time": "2022-12-30T00:15:36+00:00"
79 | },
80 | {
81 | "name": "myclabs/deep-copy",
82 | "version": "1.12.0",
83 | "source": {
84 | "type": "git",
85 | "url": "https://github.com/myclabs/DeepCopy.git",
86 | "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
87 | },
88 | "dist": {
89 | "type": "zip",
90 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
91 | "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
92 | "shasum": ""
93 | },
94 | "require": {
95 | "php": "^7.1 || ^8.0"
96 | },
97 | "conflict": {
98 | "doctrine/collections": "<1.6.8",
99 | "doctrine/common": "<2.13.3 || >=3 <3.2.2"
100 | },
101 | "require-dev": {
102 | "doctrine/collections": "^1.6.8",
103 | "doctrine/common": "^2.13.3 || ^3.2.2",
104 | "phpspec/prophecy": "^1.10",
105 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
106 | },
107 | "type": "library",
108 | "autoload": {
109 | "files": [
110 | "src/DeepCopy/deep_copy.php"
111 | ],
112 | "psr-4": {
113 | "DeepCopy\\": "src/DeepCopy/"
114 | }
115 | },
116 | "notification-url": "https://packagist.org/downloads/",
117 | "license": [
118 | "MIT"
119 | ],
120 | "description": "Create deep copies (clones) of your objects",
121 | "keywords": [
122 | "clone",
123 | "copy",
124 | "duplicate",
125 | "object",
126 | "object graph"
127 | ],
128 | "support": {
129 | "issues": "https://github.com/myclabs/DeepCopy/issues",
130 | "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
131 | },
132 | "funding": [
133 | {
134 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
135 | "type": "tidelift"
136 | }
137 | ],
138 | "time": "2024-06-12T14:39:25+00:00"
139 | },
140 | {
141 | "name": "nikic/php-parser",
142 | "version": "v4.19.1",
143 | "source": {
144 | "type": "git",
145 | "url": "https://github.com/nikic/PHP-Parser.git",
146 | "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b"
147 | },
148 | "dist": {
149 | "type": "zip",
150 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b",
151 | "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b",
152 | "shasum": ""
153 | },
154 | "require": {
155 | "ext-tokenizer": "*",
156 | "php": ">=7.1"
157 | },
158 | "require-dev": {
159 | "ircmaxell/php-yacc": "^0.0.7",
160 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
161 | },
162 | "bin": [
163 | "bin/php-parse"
164 | ],
165 | "type": "library",
166 | "extra": {
167 | "branch-alias": {
168 | "dev-master": "4.9-dev"
169 | }
170 | },
171 | "autoload": {
172 | "psr-4": {
173 | "PhpParser\\": "lib/PhpParser"
174 | }
175 | },
176 | "notification-url": "https://packagist.org/downloads/",
177 | "license": [
178 | "BSD-3-Clause"
179 | ],
180 | "authors": [
181 | {
182 | "name": "Nikita Popov"
183 | }
184 | ],
185 | "description": "A PHP parser written in PHP",
186 | "keywords": [
187 | "parser",
188 | "php"
189 | ],
190 | "support": {
191 | "issues": "https://github.com/nikic/PHP-Parser/issues",
192 | "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1"
193 | },
194 | "time": "2024-03-17T08:10:35+00:00"
195 | },
196 | {
197 | "name": "phar-io/manifest",
198 | "version": "2.0.4",
199 | "source": {
200 | "type": "git",
201 | "url": "https://github.com/phar-io/manifest.git",
202 | "reference": "54750ef60c58e43759730615a392c31c80e23176"
203 | },
204 | "dist": {
205 | "type": "zip",
206 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
207 | "reference": "54750ef60c58e43759730615a392c31c80e23176",
208 | "shasum": ""
209 | },
210 | "require": {
211 | "ext-dom": "*",
212 | "ext-libxml": "*",
213 | "ext-phar": "*",
214 | "ext-xmlwriter": "*",
215 | "phar-io/version": "^3.0.1",
216 | "php": "^7.2 || ^8.0"
217 | },
218 | "type": "library",
219 | "extra": {
220 | "branch-alias": {
221 | "dev-master": "2.0.x-dev"
222 | }
223 | },
224 | "autoload": {
225 | "classmap": [
226 | "src/"
227 | ]
228 | },
229 | "notification-url": "https://packagist.org/downloads/",
230 | "license": [
231 | "BSD-3-Clause"
232 | ],
233 | "authors": [
234 | {
235 | "name": "Arne Blankerts",
236 | "email": "arne@blankerts.de",
237 | "role": "Developer"
238 | },
239 | {
240 | "name": "Sebastian Heuer",
241 | "email": "sebastian@phpeople.de",
242 | "role": "Developer"
243 | },
244 | {
245 | "name": "Sebastian Bergmann",
246 | "email": "sebastian@phpunit.de",
247 | "role": "Developer"
248 | }
249 | ],
250 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
251 | "support": {
252 | "issues": "https://github.com/phar-io/manifest/issues",
253 | "source": "https://github.com/phar-io/manifest/tree/2.0.4"
254 | },
255 | "funding": [
256 | {
257 | "url": "https://github.com/theseer",
258 | "type": "github"
259 | }
260 | ],
261 | "time": "2024-03-03T12:33:53+00:00"
262 | },
263 | {
264 | "name": "phar-io/version",
265 | "version": "3.2.1",
266 | "source": {
267 | "type": "git",
268 | "url": "https://github.com/phar-io/version.git",
269 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
270 | },
271 | "dist": {
272 | "type": "zip",
273 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
274 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
275 | "shasum": ""
276 | },
277 | "require": {
278 | "php": "^7.2 || ^8.0"
279 | },
280 | "type": "library",
281 | "autoload": {
282 | "classmap": [
283 | "src/"
284 | ]
285 | },
286 | "notification-url": "https://packagist.org/downloads/",
287 | "license": [
288 | "BSD-3-Clause"
289 | ],
290 | "authors": [
291 | {
292 | "name": "Arne Blankerts",
293 | "email": "arne@blankerts.de",
294 | "role": "Developer"
295 | },
296 | {
297 | "name": "Sebastian Heuer",
298 | "email": "sebastian@phpeople.de",
299 | "role": "Developer"
300 | },
301 | {
302 | "name": "Sebastian Bergmann",
303 | "email": "sebastian@phpunit.de",
304 | "role": "Developer"
305 | }
306 | ],
307 | "description": "Library for handling version information and constraints",
308 | "support": {
309 | "issues": "https://github.com/phar-io/version/issues",
310 | "source": "https://github.com/phar-io/version/tree/3.2.1"
311 | },
312 | "time": "2022-02-21T01:04:05+00:00"
313 | },
314 | {
315 | "name": "phpunit/php-code-coverage",
316 | "version": "9.2.31",
317 | "source": {
318 | "type": "git",
319 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
320 | "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965"
321 | },
322 | "dist": {
323 | "type": "zip",
324 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965",
325 | "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965",
326 | "shasum": ""
327 | },
328 | "require": {
329 | "ext-dom": "*",
330 | "ext-libxml": "*",
331 | "ext-xmlwriter": "*",
332 | "nikic/php-parser": "^4.18 || ^5.0",
333 | "php": ">=7.3",
334 | "phpunit/php-file-iterator": "^3.0.3",
335 | "phpunit/php-text-template": "^2.0.2",
336 | "sebastian/code-unit-reverse-lookup": "^2.0.2",
337 | "sebastian/complexity": "^2.0",
338 | "sebastian/environment": "^5.1.2",
339 | "sebastian/lines-of-code": "^1.0.3",
340 | "sebastian/version": "^3.0.1",
341 | "theseer/tokenizer": "^1.2.0"
342 | },
343 | "require-dev": {
344 | "phpunit/phpunit": "^9.3"
345 | },
346 | "suggest": {
347 | "ext-pcov": "PHP extension that provides line coverage",
348 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
349 | },
350 | "type": "library",
351 | "extra": {
352 | "branch-alias": {
353 | "dev-master": "9.2-dev"
354 | }
355 | },
356 | "autoload": {
357 | "classmap": [
358 | "src/"
359 | ]
360 | },
361 | "notification-url": "https://packagist.org/downloads/",
362 | "license": [
363 | "BSD-3-Clause"
364 | ],
365 | "authors": [
366 | {
367 | "name": "Sebastian Bergmann",
368 | "email": "sebastian@phpunit.de",
369 | "role": "lead"
370 | }
371 | ],
372 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
373 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
374 | "keywords": [
375 | "coverage",
376 | "testing",
377 | "xunit"
378 | ],
379 | "support": {
380 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
381 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
382 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31"
383 | },
384 | "funding": [
385 | {
386 | "url": "https://github.com/sebastianbergmann",
387 | "type": "github"
388 | }
389 | ],
390 | "time": "2024-03-02T06:37:42+00:00"
391 | },
392 | {
393 | "name": "phpunit/php-file-iterator",
394 | "version": "3.0.6",
395 | "source": {
396 | "type": "git",
397 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
398 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
399 | },
400 | "dist": {
401 | "type": "zip",
402 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
403 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
404 | "shasum": ""
405 | },
406 | "require": {
407 | "php": ">=7.3"
408 | },
409 | "require-dev": {
410 | "phpunit/phpunit": "^9.3"
411 | },
412 | "type": "library",
413 | "extra": {
414 | "branch-alias": {
415 | "dev-master": "3.0-dev"
416 | }
417 | },
418 | "autoload": {
419 | "classmap": [
420 | "src/"
421 | ]
422 | },
423 | "notification-url": "https://packagist.org/downloads/",
424 | "license": [
425 | "BSD-3-Clause"
426 | ],
427 | "authors": [
428 | {
429 | "name": "Sebastian Bergmann",
430 | "email": "sebastian@phpunit.de",
431 | "role": "lead"
432 | }
433 | ],
434 | "description": "FilterIterator implementation that filters files based on a list of suffixes.",
435 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
436 | "keywords": [
437 | "filesystem",
438 | "iterator"
439 | ],
440 | "support": {
441 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
442 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
443 | },
444 | "funding": [
445 | {
446 | "url": "https://github.com/sebastianbergmann",
447 | "type": "github"
448 | }
449 | ],
450 | "time": "2021-12-02T12:48:52+00:00"
451 | },
452 | {
453 | "name": "phpunit/php-invoker",
454 | "version": "3.1.1",
455 | "source": {
456 | "type": "git",
457 | "url": "https://github.com/sebastianbergmann/php-invoker.git",
458 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
459 | },
460 | "dist": {
461 | "type": "zip",
462 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
463 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
464 | "shasum": ""
465 | },
466 | "require": {
467 | "php": ">=7.3"
468 | },
469 | "require-dev": {
470 | "ext-pcntl": "*",
471 | "phpunit/phpunit": "^9.3"
472 | },
473 | "suggest": {
474 | "ext-pcntl": "*"
475 | },
476 | "type": "library",
477 | "extra": {
478 | "branch-alias": {
479 | "dev-master": "3.1-dev"
480 | }
481 | },
482 | "autoload": {
483 | "classmap": [
484 | "src/"
485 | ]
486 | },
487 | "notification-url": "https://packagist.org/downloads/",
488 | "license": [
489 | "BSD-3-Clause"
490 | ],
491 | "authors": [
492 | {
493 | "name": "Sebastian Bergmann",
494 | "email": "sebastian@phpunit.de",
495 | "role": "lead"
496 | }
497 | ],
498 | "description": "Invoke callables with a timeout",
499 | "homepage": "https://github.com/sebastianbergmann/php-invoker/",
500 | "keywords": [
501 | "process"
502 | ],
503 | "support": {
504 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
505 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
506 | },
507 | "funding": [
508 | {
509 | "url": "https://github.com/sebastianbergmann",
510 | "type": "github"
511 | }
512 | ],
513 | "time": "2020-09-28T05:58:55+00:00"
514 | },
515 | {
516 | "name": "phpunit/php-text-template",
517 | "version": "2.0.4",
518 | "source": {
519 | "type": "git",
520 | "url": "https://github.com/sebastianbergmann/php-text-template.git",
521 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
522 | },
523 | "dist": {
524 | "type": "zip",
525 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
526 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
527 | "shasum": ""
528 | },
529 | "require": {
530 | "php": ">=7.3"
531 | },
532 | "require-dev": {
533 | "phpunit/phpunit": "^9.3"
534 | },
535 | "type": "library",
536 | "extra": {
537 | "branch-alias": {
538 | "dev-master": "2.0-dev"
539 | }
540 | },
541 | "autoload": {
542 | "classmap": [
543 | "src/"
544 | ]
545 | },
546 | "notification-url": "https://packagist.org/downloads/",
547 | "license": [
548 | "BSD-3-Clause"
549 | ],
550 | "authors": [
551 | {
552 | "name": "Sebastian Bergmann",
553 | "email": "sebastian@phpunit.de",
554 | "role": "lead"
555 | }
556 | ],
557 | "description": "Simple template engine.",
558 | "homepage": "https://github.com/sebastianbergmann/php-text-template/",
559 | "keywords": [
560 | "template"
561 | ],
562 | "support": {
563 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
564 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
565 | },
566 | "funding": [
567 | {
568 | "url": "https://github.com/sebastianbergmann",
569 | "type": "github"
570 | }
571 | ],
572 | "time": "2020-10-26T05:33:50+00:00"
573 | },
574 | {
575 | "name": "phpunit/php-timer",
576 | "version": "5.0.3",
577 | "source": {
578 | "type": "git",
579 | "url": "https://github.com/sebastianbergmann/php-timer.git",
580 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
581 | },
582 | "dist": {
583 | "type": "zip",
584 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
585 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
586 | "shasum": ""
587 | },
588 | "require": {
589 | "php": ">=7.3"
590 | },
591 | "require-dev": {
592 | "phpunit/phpunit": "^9.3"
593 | },
594 | "type": "library",
595 | "extra": {
596 | "branch-alias": {
597 | "dev-master": "5.0-dev"
598 | }
599 | },
600 | "autoload": {
601 | "classmap": [
602 | "src/"
603 | ]
604 | },
605 | "notification-url": "https://packagist.org/downloads/",
606 | "license": [
607 | "BSD-3-Clause"
608 | ],
609 | "authors": [
610 | {
611 | "name": "Sebastian Bergmann",
612 | "email": "sebastian@phpunit.de",
613 | "role": "lead"
614 | }
615 | ],
616 | "description": "Utility class for timing",
617 | "homepage": "https://github.com/sebastianbergmann/php-timer/",
618 | "keywords": [
619 | "timer"
620 | ],
621 | "support": {
622 | "issues": "https://github.com/sebastianbergmann/php-timer/issues",
623 | "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
624 | },
625 | "funding": [
626 | {
627 | "url": "https://github.com/sebastianbergmann",
628 | "type": "github"
629 | }
630 | ],
631 | "time": "2020-10-26T13:16:10+00:00"
632 | },
633 | {
634 | "name": "phpunit/phpunit",
635 | "version": "9.6.19",
636 | "source": {
637 | "type": "git",
638 | "url": "https://github.com/sebastianbergmann/phpunit.git",
639 | "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
640 | },
641 | "dist": {
642 | "type": "zip",
643 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
644 | "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
645 | "shasum": ""
646 | },
647 | "require": {
648 | "doctrine/instantiator": "^1.3.1 || ^2",
649 | "ext-dom": "*",
650 | "ext-json": "*",
651 | "ext-libxml": "*",
652 | "ext-mbstring": "*",
653 | "ext-xml": "*",
654 | "ext-xmlwriter": "*",
655 | "myclabs/deep-copy": "^1.10.1",
656 | "phar-io/manifest": "^2.0.3",
657 | "phar-io/version": "^3.0.2",
658 | "php": ">=7.3",
659 | "phpunit/php-code-coverage": "^9.2.28",
660 | "phpunit/php-file-iterator": "^3.0.5",
661 | "phpunit/php-invoker": "^3.1.1",
662 | "phpunit/php-text-template": "^2.0.3",
663 | "phpunit/php-timer": "^5.0.2",
664 | "sebastian/cli-parser": "^1.0.1",
665 | "sebastian/code-unit": "^1.0.6",
666 | "sebastian/comparator": "^4.0.8",
667 | "sebastian/diff": "^4.0.3",
668 | "sebastian/environment": "^5.1.3",
669 | "sebastian/exporter": "^4.0.5",
670 | "sebastian/global-state": "^5.0.1",
671 | "sebastian/object-enumerator": "^4.0.3",
672 | "sebastian/resource-operations": "^3.0.3",
673 | "sebastian/type": "^3.2",
674 | "sebastian/version": "^3.0.2"
675 | },
676 | "suggest": {
677 | "ext-soap": "To be able to generate mocks based on WSDL files",
678 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
679 | },
680 | "bin": [
681 | "phpunit"
682 | ],
683 | "type": "library",
684 | "extra": {
685 | "branch-alias": {
686 | "dev-master": "9.6-dev"
687 | }
688 | },
689 | "autoload": {
690 | "files": [
691 | "src/Framework/Assert/Functions.php"
692 | ],
693 | "classmap": [
694 | "src/"
695 | ]
696 | },
697 | "notification-url": "https://packagist.org/downloads/",
698 | "license": [
699 | "BSD-3-Clause"
700 | ],
701 | "authors": [
702 | {
703 | "name": "Sebastian Bergmann",
704 | "email": "sebastian@phpunit.de",
705 | "role": "lead"
706 | }
707 | ],
708 | "description": "The PHP Unit Testing framework.",
709 | "homepage": "https://phpunit.de/",
710 | "keywords": [
711 | "phpunit",
712 | "testing",
713 | "xunit"
714 | ],
715 | "support": {
716 | "issues": "https://github.com/sebastianbergmann/phpunit/issues",
717 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
718 | "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
719 | },
720 | "funding": [
721 | {
722 | "url": "https://phpunit.de/sponsors.html",
723 | "type": "custom"
724 | },
725 | {
726 | "url": "https://github.com/sebastianbergmann",
727 | "type": "github"
728 | },
729 | {
730 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
731 | "type": "tidelift"
732 | }
733 | ],
734 | "time": "2024-04-05T04:35:58+00:00"
735 | },
736 | {
737 | "name": "sebastian/cli-parser",
738 | "version": "1.0.2",
739 | "source": {
740 | "type": "git",
741 | "url": "https://github.com/sebastianbergmann/cli-parser.git",
742 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
743 | },
744 | "dist": {
745 | "type": "zip",
746 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
747 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
748 | "shasum": ""
749 | },
750 | "require": {
751 | "php": ">=7.3"
752 | },
753 | "require-dev": {
754 | "phpunit/phpunit": "^9.3"
755 | },
756 | "type": "library",
757 | "extra": {
758 | "branch-alias": {
759 | "dev-master": "1.0-dev"
760 | }
761 | },
762 | "autoload": {
763 | "classmap": [
764 | "src/"
765 | ]
766 | },
767 | "notification-url": "https://packagist.org/downloads/",
768 | "license": [
769 | "BSD-3-Clause"
770 | ],
771 | "authors": [
772 | {
773 | "name": "Sebastian Bergmann",
774 | "email": "sebastian@phpunit.de",
775 | "role": "lead"
776 | }
777 | ],
778 | "description": "Library for parsing CLI options",
779 | "homepage": "https://github.com/sebastianbergmann/cli-parser",
780 | "support": {
781 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
782 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
783 | },
784 | "funding": [
785 | {
786 | "url": "https://github.com/sebastianbergmann",
787 | "type": "github"
788 | }
789 | ],
790 | "time": "2024-03-02T06:27:43+00:00"
791 | },
792 | {
793 | "name": "sebastian/code-unit",
794 | "version": "1.0.8",
795 | "source": {
796 | "type": "git",
797 | "url": "https://github.com/sebastianbergmann/code-unit.git",
798 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
799 | },
800 | "dist": {
801 | "type": "zip",
802 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
803 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
804 | "shasum": ""
805 | },
806 | "require": {
807 | "php": ">=7.3"
808 | },
809 | "require-dev": {
810 | "phpunit/phpunit": "^9.3"
811 | },
812 | "type": "library",
813 | "extra": {
814 | "branch-alias": {
815 | "dev-master": "1.0-dev"
816 | }
817 | },
818 | "autoload": {
819 | "classmap": [
820 | "src/"
821 | ]
822 | },
823 | "notification-url": "https://packagist.org/downloads/",
824 | "license": [
825 | "BSD-3-Clause"
826 | ],
827 | "authors": [
828 | {
829 | "name": "Sebastian Bergmann",
830 | "email": "sebastian@phpunit.de",
831 | "role": "lead"
832 | }
833 | ],
834 | "description": "Collection of value objects that represent the PHP code units",
835 | "homepage": "https://github.com/sebastianbergmann/code-unit",
836 | "support": {
837 | "issues": "https://github.com/sebastianbergmann/code-unit/issues",
838 | "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
839 | },
840 | "funding": [
841 | {
842 | "url": "https://github.com/sebastianbergmann",
843 | "type": "github"
844 | }
845 | ],
846 | "time": "2020-10-26T13:08:54+00:00"
847 | },
848 | {
849 | "name": "sebastian/code-unit-reverse-lookup",
850 | "version": "2.0.3",
851 | "source": {
852 | "type": "git",
853 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
854 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
855 | },
856 | "dist": {
857 | "type": "zip",
858 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
859 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
860 | "shasum": ""
861 | },
862 | "require": {
863 | "php": ">=7.3"
864 | },
865 | "require-dev": {
866 | "phpunit/phpunit": "^9.3"
867 | },
868 | "type": "library",
869 | "extra": {
870 | "branch-alias": {
871 | "dev-master": "2.0-dev"
872 | }
873 | },
874 | "autoload": {
875 | "classmap": [
876 | "src/"
877 | ]
878 | },
879 | "notification-url": "https://packagist.org/downloads/",
880 | "license": [
881 | "BSD-3-Clause"
882 | ],
883 | "authors": [
884 | {
885 | "name": "Sebastian Bergmann",
886 | "email": "sebastian@phpunit.de"
887 | }
888 | ],
889 | "description": "Looks up which function or method a line of code belongs to",
890 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
891 | "support": {
892 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
893 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
894 | },
895 | "funding": [
896 | {
897 | "url": "https://github.com/sebastianbergmann",
898 | "type": "github"
899 | }
900 | ],
901 | "time": "2020-09-28T05:30:19+00:00"
902 | },
903 | {
904 | "name": "sebastian/comparator",
905 | "version": "4.0.8",
906 | "source": {
907 | "type": "git",
908 | "url": "https://github.com/sebastianbergmann/comparator.git",
909 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
910 | },
911 | "dist": {
912 | "type": "zip",
913 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
914 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
915 | "shasum": ""
916 | },
917 | "require": {
918 | "php": ">=7.3",
919 | "sebastian/diff": "^4.0",
920 | "sebastian/exporter": "^4.0"
921 | },
922 | "require-dev": {
923 | "phpunit/phpunit": "^9.3"
924 | },
925 | "type": "library",
926 | "extra": {
927 | "branch-alias": {
928 | "dev-master": "4.0-dev"
929 | }
930 | },
931 | "autoload": {
932 | "classmap": [
933 | "src/"
934 | ]
935 | },
936 | "notification-url": "https://packagist.org/downloads/",
937 | "license": [
938 | "BSD-3-Clause"
939 | ],
940 | "authors": [
941 | {
942 | "name": "Sebastian Bergmann",
943 | "email": "sebastian@phpunit.de"
944 | },
945 | {
946 | "name": "Jeff Welch",
947 | "email": "whatthejeff@gmail.com"
948 | },
949 | {
950 | "name": "Volker Dusch",
951 | "email": "github@wallbash.com"
952 | },
953 | {
954 | "name": "Bernhard Schussek",
955 | "email": "bschussek@2bepublished.at"
956 | }
957 | ],
958 | "description": "Provides the functionality to compare PHP values for equality",
959 | "homepage": "https://github.com/sebastianbergmann/comparator",
960 | "keywords": [
961 | "comparator",
962 | "compare",
963 | "equality"
964 | ],
965 | "support": {
966 | "issues": "https://github.com/sebastianbergmann/comparator/issues",
967 | "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
968 | },
969 | "funding": [
970 | {
971 | "url": "https://github.com/sebastianbergmann",
972 | "type": "github"
973 | }
974 | ],
975 | "time": "2022-09-14T12:41:17+00:00"
976 | },
977 | {
978 | "name": "sebastian/complexity",
979 | "version": "2.0.3",
980 | "source": {
981 | "type": "git",
982 | "url": "https://github.com/sebastianbergmann/complexity.git",
983 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
984 | },
985 | "dist": {
986 | "type": "zip",
987 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
988 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
989 | "shasum": ""
990 | },
991 | "require": {
992 | "nikic/php-parser": "^4.18 || ^5.0",
993 | "php": ">=7.3"
994 | },
995 | "require-dev": {
996 | "phpunit/phpunit": "^9.3"
997 | },
998 | "type": "library",
999 | "extra": {
1000 | "branch-alias": {
1001 | "dev-master": "2.0-dev"
1002 | }
1003 | },
1004 | "autoload": {
1005 | "classmap": [
1006 | "src/"
1007 | ]
1008 | },
1009 | "notification-url": "https://packagist.org/downloads/",
1010 | "license": [
1011 | "BSD-3-Clause"
1012 | ],
1013 | "authors": [
1014 | {
1015 | "name": "Sebastian Bergmann",
1016 | "email": "sebastian@phpunit.de",
1017 | "role": "lead"
1018 | }
1019 | ],
1020 | "description": "Library for calculating the complexity of PHP code units",
1021 | "homepage": "https://github.com/sebastianbergmann/complexity",
1022 | "support": {
1023 | "issues": "https://github.com/sebastianbergmann/complexity/issues",
1024 | "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
1025 | },
1026 | "funding": [
1027 | {
1028 | "url": "https://github.com/sebastianbergmann",
1029 | "type": "github"
1030 | }
1031 | ],
1032 | "time": "2023-12-22T06:19:30+00:00"
1033 | },
1034 | {
1035 | "name": "sebastian/diff",
1036 | "version": "4.0.6",
1037 | "source": {
1038 | "type": "git",
1039 | "url": "https://github.com/sebastianbergmann/diff.git",
1040 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
1041 | },
1042 | "dist": {
1043 | "type": "zip",
1044 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
1045 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
1046 | "shasum": ""
1047 | },
1048 | "require": {
1049 | "php": ">=7.3"
1050 | },
1051 | "require-dev": {
1052 | "phpunit/phpunit": "^9.3",
1053 | "symfony/process": "^4.2 || ^5"
1054 | },
1055 | "type": "library",
1056 | "extra": {
1057 | "branch-alias": {
1058 | "dev-master": "4.0-dev"
1059 | }
1060 | },
1061 | "autoload": {
1062 | "classmap": [
1063 | "src/"
1064 | ]
1065 | },
1066 | "notification-url": "https://packagist.org/downloads/",
1067 | "license": [
1068 | "BSD-3-Clause"
1069 | ],
1070 | "authors": [
1071 | {
1072 | "name": "Sebastian Bergmann",
1073 | "email": "sebastian@phpunit.de"
1074 | },
1075 | {
1076 | "name": "Kore Nordmann",
1077 | "email": "mail@kore-nordmann.de"
1078 | }
1079 | ],
1080 | "description": "Diff implementation",
1081 | "homepage": "https://github.com/sebastianbergmann/diff",
1082 | "keywords": [
1083 | "diff",
1084 | "udiff",
1085 | "unidiff",
1086 | "unified diff"
1087 | ],
1088 | "support": {
1089 | "issues": "https://github.com/sebastianbergmann/diff/issues",
1090 | "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
1091 | },
1092 | "funding": [
1093 | {
1094 | "url": "https://github.com/sebastianbergmann",
1095 | "type": "github"
1096 | }
1097 | ],
1098 | "time": "2024-03-02T06:30:58+00:00"
1099 | },
1100 | {
1101 | "name": "sebastian/environment",
1102 | "version": "5.1.5",
1103 | "source": {
1104 | "type": "git",
1105 | "url": "https://github.com/sebastianbergmann/environment.git",
1106 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
1107 | },
1108 | "dist": {
1109 | "type": "zip",
1110 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
1111 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
1112 | "shasum": ""
1113 | },
1114 | "require": {
1115 | "php": ">=7.3"
1116 | },
1117 | "require-dev": {
1118 | "phpunit/phpunit": "^9.3"
1119 | },
1120 | "suggest": {
1121 | "ext-posix": "*"
1122 | },
1123 | "type": "library",
1124 | "extra": {
1125 | "branch-alias": {
1126 | "dev-master": "5.1-dev"
1127 | }
1128 | },
1129 | "autoload": {
1130 | "classmap": [
1131 | "src/"
1132 | ]
1133 | },
1134 | "notification-url": "https://packagist.org/downloads/",
1135 | "license": [
1136 | "BSD-3-Clause"
1137 | ],
1138 | "authors": [
1139 | {
1140 | "name": "Sebastian Bergmann",
1141 | "email": "sebastian@phpunit.de"
1142 | }
1143 | ],
1144 | "description": "Provides functionality to handle HHVM/PHP environments",
1145 | "homepage": "http://www.github.com/sebastianbergmann/environment",
1146 | "keywords": [
1147 | "Xdebug",
1148 | "environment",
1149 | "hhvm"
1150 | ],
1151 | "support": {
1152 | "issues": "https://github.com/sebastianbergmann/environment/issues",
1153 | "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
1154 | },
1155 | "funding": [
1156 | {
1157 | "url": "https://github.com/sebastianbergmann",
1158 | "type": "github"
1159 | }
1160 | ],
1161 | "time": "2023-02-03T06:03:51+00:00"
1162 | },
1163 | {
1164 | "name": "sebastian/exporter",
1165 | "version": "4.0.6",
1166 | "source": {
1167 | "type": "git",
1168 | "url": "https://github.com/sebastianbergmann/exporter.git",
1169 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
1170 | },
1171 | "dist": {
1172 | "type": "zip",
1173 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
1174 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
1175 | "shasum": ""
1176 | },
1177 | "require": {
1178 | "php": ">=7.3",
1179 | "sebastian/recursion-context": "^4.0"
1180 | },
1181 | "require-dev": {
1182 | "ext-mbstring": "*",
1183 | "phpunit/phpunit": "^9.3"
1184 | },
1185 | "type": "library",
1186 | "extra": {
1187 | "branch-alias": {
1188 | "dev-master": "4.0-dev"
1189 | }
1190 | },
1191 | "autoload": {
1192 | "classmap": [
1193 | "src/"
1194 | ]
1195 | },
1196 | "notification-url": "https://packagist.org/downloads/",
1197 | "license": [
1198 | "BSD-3-Clause"
1199 | ],
1200 | "authors": [
1201 | {
1202 | "name": "Sebastian Bergmann",
1203 | "email": "sebastian@phpunit.de"
1204 | },
1205 | {
1206 | "name": "Jeff Welch",
1207 | "email": "whatthejeff@gmail.com"
1208 | },
1209 | {
1210 | "name": "Volker Dusch",
1211 | "email": "github@wallbash.com"
1212 | },
1213 | {
1214 | "name": "Adam Harvey",
1215 | "email": "aharvey@php.net"
1216 | },
1217 | {
1218 | "name": "Bernhard Schussek",
1219 | "email": "bschussek@gmail.com"
1220 | }
1221 | ],
1222 | "description": "Provides the functionality to export PHP variables for visualization",
1223 | "homepage": "https://www.github.com/sebastianbergmann/exporter",
1224 | "keywords": [
1225 | "export",
1226 | "exporter"
1227 | ],
1228 | "support": {
1229 | "issues": "https://github.com/sebastianbergmann/exporter/issues",
1230 | "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
1231 | },
1232 | "funding": [
1233 | {
1234 | "url": "https://github.com/sebastianbergmann",
1235 | "type": "github"
1236 | }
1237 | ],
1238 | "time": "2024-03-02T06:33:00+00:00"
1239 | },
1240 | {
1241 | "name": "sebastian/global-state",
1242 | "version": "5.0.7",
1243 | "source": {
1244 | "type": "git",
1245 | "url": "https://github.com/sebastianbergmann/global-state.git",
1246 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
1247 | },
1248 | "dist": {
1249 | "type": "zip",
1250 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
1251 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
1252 | "shasum": ""
1253 | },
1254 | "require": {
1255 | "php": ">=7.3",
1256 | "sebastian/object-reflector": "^2.0",
1257 | "sebastian/recursion-context": "^4.0"
1258 | },
1259 | "require-dev": {
1260 | "ext-dom": "*",
1261 | "phpunit/phpunit": "^9.3"
1262 | },
1263 | "suggest": {
1264 | "ext-uopz": "*"
1265 | },
1266 | "type": "library",
1267 | "extra": {
1268 | "branch-alias": {
1269 | "dev-master": "5.0-dev"
1270 | }
1271 | },
1272 | "autoload": {
1273 | "classmap": [
1274 | "src/"
1275 | ]
1276 | },
1277 | "notification-url": "https://packagist.org/downloads/",
1278 | "license": [
1279 | "BSD-3-Clause"
1280 | ],
1281 | "authors": [
1282 | {
1283 | "name": "Sebastian Bergmann",
1284 | "email": "sebastian@phpunit.de"
1285 | }
1286 | ],
1287 | "description": "Snapshotting of global state",
1288 | "homepage": "http://www.github.com/sebastianbergmann/global-state",
1289 | "keywords": [
1290 | "global state"
1291 | ],
1292 | "support": {
1293 | "issues": "https://github.com/sebastianbergmann/global-state/issues",
1294 | "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
1295 | },
1296 | "funding": [
1297 | {
1298 | "url": "https://github.com/sebastianbergmann",
1299 | "type": "github"
1300 | }
1301 | ],
1302 | "time": "2024-03-02T06:35:11+00:00"
1303 | },
1304 | {
1305 | "name": "sebastian/lines-of-code",
1306 | "version": "1.0.4",
1307 | "source": {
1308 | "type": "git",
1309 | "url": "https://github.com/sebastianbergmann/lines-of-code.git",
1310 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
1311 | },
1312 | "dist": {
1313 | "type": "zip",
1314 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
1315 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
1316 | "shasum": ""
1317 | },
1318 | "require": {
1319 | "nikic/php-parser": "^4.18 || ^5.0",
1320 | "php": ">=7.3"
1321 | },
1322 | "require-dev": {
1323 | "phpunit/phpunit": "^9.3"
1324 | },
1325 | "type": "library",
1326 | "extra": {
1327 | "branch-alias": {
1328 | "dev-master": "1.0-dev"
1329 | }
1330 | },
1331 | "autoload": {
1332 | "classmap": [
1333 | "src/"
1334 | ]
1335 | },
1336 | "notification-url": "https://packagist.org/downloads/",
1337 | "license": [
1338 | "BSD-3-Clause"
1339 | ],
1340 | "authors": [
1341 | {
1342 | "name": "Sebastian Bergmann",
1343 | "email": "sebastian@phpunit.de",
1344 | "role": "lead"
1345 | }
1346 | ],
1347 | "description": "Library for counting the lines of code in PHP source code",
1348 | "homepage": "https://github.com/sebastianbergmann/lines-of-code",
1349 | "support": {
1350 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
1351 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
1352 | },
1353 | "funding": [
1354 | {
1355 | "url": "https://github.com/sebastianbergmann",
1356 | "type": "github"
1357 | }
1358 | ],
1359 | "time": "2023-12-22T06:20:34+00:00"
1360 | },
1361 | {
1362 | "name": "sebastian/object-enumerator",
1363 | "version": "4.0.4",
1364 | "source": {
1365 | "type": "git",
1366 | "url": "https://github.com/sebastianbergmann/object-enumerator.git",
1367 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
1368 | },
1369 | "dist": {
1370 | "type": "zip",
1371 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
1372 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
1373 | "shasum": ""
1374 | },
1375 | "require": {
1376 | "php": ">=7.3",
1377 | "sebastian/object-reflector": "^2.0",
1378 | "sebastian/recursion-context": "^4.0"
1379 | },
1380 | "require-dev": {
1381 | "phpunit/phpunit": "^9.3"
1382 | },
1383 | "type": "library",
1384 | "extra": {
1385 | "branch-alias": {
1386 | "dev-master": "4.0-dev"
1387 | }
1388 | },
1389 | "autoload": {
1390 | "classmap": [
1391 | "src/"
1392 | ]
1393 | },
1394 | "notification-url": "https://packagist.org/downloads/",
1395 | "license": [
1396 | "BSD-3-Clause"
1397 | ],
1398 | "authors": [
1399 | {
1400 | "name": "Sebastian Bergmann",
1401 | "email": "sebastian@phpunit.de"
1402 | }
1403 | ],
1404 | "description": "Traverses array structures and object graphs to enumerate all referenced objects",
1405 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
1406 | "support": {
1407 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
1408 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
1409 | },
1410 | "funding": [
1411 | {
1412 | "url": "https://github.com/sebastianbergmann",
1413 | "type": "github"
1414 | }
1415 | ],
1416 | "time": "2020-10-26T13:12:34+00:00"
1417 | },
1418 | {
1419 | "name": "sebastian/object-reflector",
1420 | "version": "2.0.4",
1421 | "source": {
1422 | "type": "git",
1423 | "url": "https://github.com/sebastianbergmann/object-reflector.git",
1424 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
1425 | },
1426 | "dist": {
1427 | "type": "zip",
1428 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
1429 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
1430 | "shasum": ""
1431 | },
1432 | "require": {
1433 | "php": ">=7.3"
1434 | },
1435 | "require-dev": {
1436 | "phpunit/phpunit": "^9.3"
1437 | },
1438 | "type": "library",
1439 | "extra": {
1440 | "branch-alias": {
1441 | "dev-master": "2.0-dev"
1442 | }
1443 | },
1444 | "autoload": {
1445 | "classmap": [
1446 | "src/"
1447 | ]
1448 | },
1449 | "notification-url": "https://packagist.org/downloads/",
1450 | "license": [
1451 | "BSD-3-Clause"
1452 | ],
1453 | "authors": [
1454 | {
1455 | "name": "Sebastian Bergmann",
1456 | "email": "sebastian@phpunit.de"
1457 | }
1458 | ],
1459 | "description": "Allows reflection of object attributes, including inherited and non-public ones",
1460 | "homepage": "https://github.com/sebastianbergmann/object-reflector/",
1461 | "support": {
1462 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
1463 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
1464 | },
1465 | "funding": [
1466 | {
1467 | "url": "https://github.com/sebastianbergmann",
1468 | "type": "github"
1469 | }
1470 | ],
1471 | "time": "2020-10-26T13:14:26+00:00"
1472 | },
1473 | {
1474 | "name": "sebastian/recursion-context",
1475 | "version": "4.0.5",
1476 | "source": {
1477 | "type": "git",
1478 | "url": "https://github.com/sebastianbergmann/recursion-context.git",
1479 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
1480 | },
1481 | "dist": {
1482 | "type": "zip",
1483 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
1484 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
1485 | "shasum": ""
1486 | },
1487 | "require": {
1488 | "php": ">=7.3"
1489 | },
1490 | "require-dev": {
1491 | "phpunit/phpunit": "^9.3"
1492 | },
1493 | "type": "library",
1494 | "extra": {
1495 | "branch-alias": {
1496 | "dev-master": "4.0-dev"
1497 | }
1498 | },
1499 | "autoload": {
1500 | "classmap": [
1501 | "src/"
1502 | ]
1503 | },
1504 | "notification-url": "https://packagist.org/downloads/",
1505 | "license": [
1506 | "BSD-3-Clause"
1507 | ],
1508 | "authors": [
1509 | {
1510 | "name": "Sebastian Bergmann",
1511 | "email": "sebastian@phpunit.de"
1512 | },
1513 | {
1514 | "name": "Jeff Welch",
1515 | "email": "whatthejeff@gmail.com"
1516 | },
1517 | {
1518 | "name": "Adam Harvey",
1519 | "email": "aharvey@php.net"
1520 | }
1521 | ],
1522 | "description": "Provides functionality to recursively process PHP variables",
1523 | "homepage": "https://github.com/sebastianbergmann/recursion-context",
1524 | "support": {
1525 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
1526 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
1527 | },
1528 | "funding": [
1529 | {
1530 | "url": "https://github.com/sebastianbergmann",
1531 | "type": "github"
1532 | }
1533 | ],
1534 | "time": "2023-02-03T06:07:39+00:00"
1535 | },
1536 | {
1537 | "name": "sebastian/resource-operations",
1538 | "version": "3.0.4",
1539 | "source": {
1540 | "type": "git",
1541 | "url": "https://github.com/sebastianbergmann/resource-operations.git",
1542 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
1543 | },
1544 | "dist": {
1545 | "type": "zip",
1546 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
1547 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
1548 | "shasum": ""
1549 | },
1550 | "require": {
1551 | "php": ">=7.3"
1552 | },
1553 | "require-dev": {
1554 | "phpunit/phpunit": "^9.0"
1555 | },
1556 | "type": "library",
1557 | "extra": {
1558 | "branch-alias": {
1559 | "dev-main": "3.0-dev"
1560 | }
1561 | },
1562 | "autoload": {
1563 | "classmap": [
1564 | "src/"
1565 | ]
1566 | },
1567 | "notification-url": "https://packagist.org/downloads/",
1568 | "license": [
1569 | "BSD-3-Clause"
1570 | ],
1571 | "authors": [
1572 | {
1573 | "name": "Sebastian Bergmann",
1574 | "email": "sebastian@phpunit.de"
1575 | }
1576 | ],
1577 | "description": "Provides a list of PHP built-in functions that operate on resources",
1578 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
1579 | "support": {
1580 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
1581 | },
1582 | "funding": [
1583 | {
1584 | "url": "https://github.com/sebastianbergmann",
1585 | "type": "github"
1586 | }
1587 | ],
1588 | "time": "2024-03-14T16:00:52+00:00"
1589 | },
1590 | {
1591 | "name": "sebastian/type",
1592 | "version": "3.2.1",
1593 | "source": {
1594 | "type": "git",
1595 | "url": "https://github.com/sebastianbergmann/type.git",
1596 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
1597 | },
1598 | "dist": {
1599 | "type": "zip",
1600 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
1601 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
1602 | "shasum": ""
1603 | },
1604 | "require": {
1605 | "php": ">=7.3"
1606 | },
1607 | "require-dev": {
1608 | "phpunit/phpunit": "^9.5"
1609 | },
1610 | "type": "library",
1611 | "extra": {
1612 | "branch-alias": {
1613 | "dev-master": "3.2-dev"
1614 | }
1615 | },
1616 | "autoload": {
1617 | "classmap": [
1618 | "src/"
1619 | ]
1620 | },
1621 | "notification-url": "https://packagist.org/downloads/",
1622 | "license": [
1623 | "BSD-3-Clause"
1624 | ],
1625 | "authors": [
1626 | {
1627 | "name": "Sebastian Bergmann",
1628 | "email": "sebastian@phpunit.de",
1629 | "role": "lead"
1630 | }
1631 | ],
1632 | "description": "Collection of value objects that represent the types of the PHP type system",
1633 | "homepage": "https://github.com/sebastianbergmann/type",
1634 | "support": {
1635 | "issues": "https://github.com/sebastianbergmann/type/issues",
1636 | "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
1637 | },
1638 | "funding": [
1639 | {
1640 | "url": "https://github.com/sebastianbergmann",
1641 | "type": "github"
1642 | }
1643 | ],
1644 | "time": "2023-02-03T06:13:03+00:00"
1645 | },
1646 | {
1647 | "name": "sebastian/version",
1648 | "version": "3.0.2",
1649 | "source": {
1650 | "type": "git",
1651 | "url": "https://github.com/sebastianbergmann/version.git",
1652 | "reference": "c6c1022351a901512170118436c764e473f6de8c"
1653 | },
1654 | "dist": {
1655 | "type": "zip",
1656 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
1657 | "reference": "c6c1022351a901512170118436c764e473f6de8c",
1658 | "shasum": ""
1659 | },
1660 | "require": {
1661 | "php": ">=7.3"
1662 | },
1663 | "type": "library",
1664 | "extra": {
1665 | "branch-alias": {
1666 | "dev-master": "3.0-dev"
1667 | }
1668 | },
1669 | "autoload": {
1670 | "classmap": [
1671 | "src/"
1672 | ]
1673 | },
1674 | "notification-url": "https://packagist.org/downloads/",
1675 | "license": [
1676 | "BSD-3-Clause"
1677 | ],
1678 | "authors": [
1679 | {
1680 | "name": "Sebastian Bergmann",
1681 | "email": "sebastian@phpunit.de",
1682 | "role": "lead"
1683 | }
1684 | ],
1685 | "description": "Library that helps with managing the version number of Git-hosted PHP projects",
1686 | "homepage": "https://github.com/sebastianbergmann/version",
1687 | "support": {
1688 | "issues": "https://github.com/sebastianbergmann/version/issues",
1689 | "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
1690 | },
1691 | "funding": [
1692 | {
1693 | "url": "https://github.com/sebastianbergmann",
1694 | "type": "github"
1695 | }
1696 | ],
1697 | "time": "2020-09-28T06:39:44+00:00"
1698 | },
1699 | {
1700 | "name": "theseer/tokenizer",
1701 | "version": "1.2.3",
1702 | "source": {
1703 | "type": "git",
1704 | "url": "https://github.com/theseer/tokenizer.git",
1705 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
1706 | },
1707 | "dist": {
1708 | "type": "zip",
1709 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
1710 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
1711 | "shasum": ""
1712 | },
1713 | "require": {
1714 | "ext-dom": "*",
1715 | "ext-tokenizer": "*",
1716 | "ext-xmlwriter": "*",
1717 | "php": "^7.2 || ^8.0"
1718 | },
1719 | "type": "library",
1720 | "autoload": {
1721 | "classmap": [
1722 | "src/"
1723 | ]
1724 | },
1725 | "notification-url": "https://packagist.org/downloads/",
1726 | "license": [
1727 | "BSD-3-Clause"
1728 | ],
1729 | "authors": [
1730 | {
1731 | "name": "Arne Blankerts",
1732 | "email": "arne@blankerts.de",
1733 | "role": "Developer"
1734 | }
1735 | ],
1736 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
1737 | "support": {
1738 | "issues": "https://github.com/theseer/tokenizer/issues",
1739 | "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
1740 | },
1741 | "funding": [
1742 | {
1743 | "url": "https://github.com/theseer",
1744 | "type": "github"
1745 | }
1746 | ],
1747 | "time": "2024-03-03T12:36:25+00:00"
1748 | }
1749 | ],
1750 | "aliases": [],
1751 | "minimum-stability": "stable",
1752 | "stability-flags": [],
1753 | "prefer-stable": false,
1754 | "prefer-lowest": false,
1755 | "platform": {
1756 | "ext-json": "*",
1757 | "php": ">=7.3"
1758 | },
1759 | "platform-dev": [],
1760 | "plugin-api-version": "2.2.0"
1761 | }
1762 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | web:
3 | build: .
4 | working_dir: /var/www/html/
5 | volumes:
6 | - ".:/var/www/html/"
7 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 | test/Escher/Test/Unit/
15 |
16 |
17 | test/Escher/Test/EndToEnd/
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/repo-info.json:
--------------------------------------------------------------------------------
1 | {
2 | "is_in_production": true,
3 | "is_scannable": true,
4 | "is_critical": false,
5 | "contact": "G-GSUITE-Security@emarsys.com",
6 | "hosted": null
7 | }
8 |
--------------------------------------------------------------------------------
/src/Escher/AuthElements.php:
--------------------------------------------------------------------------------
1 | elementParts = $elementParts;
21 | $this->accessKeyId = $accessKeyId;
22 | $this->shortDate = $shortDate;
23 | $this->credentialScope = $credentialScope;
24 | $this->dateTime = $dateTime;
25 | $this->host = $host;
26 | $this->isFromHeaders = $isFromHeaders;
27 | }
28 |
29 | /**
30 | * @param array $headerList
31 | * @param $authHeaderKey
32 | * @param $dateHeaderKey
33 | * @param $algoPrefix
34 | * @return AuthElements
35 | * @throws Exception
36 | */
37 | public static function parseFromHeaders(array $headerList, $authHeaderKey, $dateHeaderKey, $algoPrefix)
38 | {
39 | $headerList = Utils::keysToLower($headerList);
40 | $elementParts = self::parseAuthHeader($headerList[strtolower($authHeaderKey)], $algoPrefix);
41 | list($accessKeyId, $shortDate, $credentialScope) = explode('/', $elementParts['Credentials'], 3);
42 | $host = self::checkHost($headerList);
43 |
44 | if (!isset($headerList[strtolower($dateHeaderKey)])) {
45 | throw new Exception('The '.strtolower($dateHeaderKey).' header is missing', Exception::CODE_MISSING_HEADER_PARAM);
46 | }
47 |
48 | if (strtolower($dateHeaderKey) !== 'date') {
49 | $dateTime = Utils::parseLongDate($headerList[strtolower($dateHeaderKey)]);
50 | } else {
51 | try {
52 | $dateTime = new DateTime($headerList[strtolower($dateHeaderKey)], new DateTimeZone('GMT'));
53 | } catch (\Exception $ex) {
54 | throw new Exception('Date header is invalid, the expected format is Wed, 04 Nov 2015 09:20:22 GMT', Exception::CODE_FORMAT_INVALID_DATE_HEADER_GMT);
55 | }
56 | }
57 | return new AuthElements($elementParts, $accessKeyId, $shortDate, $credentialScope, $dateTime, $host, true);
58 | }
59 |
60 | /**
61 | * @param $headerContent
62 | * @param $algoPrefix
63 | * @return array
64 | * @throws Exception
65 | */
66 | public static function parseAuthHeader($headerContent, $algoPrefix)
67 | {
68 | $pattern = '/^' . $algoPrefix . '-HMAC-([A-Z0-9\,]+)(.*)' .
69 | 'Credential=([A-Za-z0-9\/\-_ ]+),(.*)' .
70 | 'SignedHeaders=([A-Za-z\-;]+),(.*)' .
71 | 'Signature=([0-9a-f]+)$/';
72 |
73 | if (!preg_match($pattern, $headerContent, $matches)) {
74 | throw new Exception('Could not parse auth header', Exception::CODE_FORMAT_INVALID_AUTH_HEADER);
75 | }
76 | return [
77 | 'Algorithm' => $matches[1],
78 | 'Credentials' => $matches[3],
79 | 'SignedHeaders' => $matches[5],
80 | 'Signature' => $matches[7],
81 | ];
82 | }
83 |
84 | public static function parseFromQuery($headerList, $queryParams, $vendorKey, $algoPrefix)
85 | {
86 | $elementParts = [];
87 | $paramKey = self::checkParam($queryParams, $vendorKey, 'Algorithm');
88 |
89 | $pattern = '/^' . $algoPrefix . '-HMAC-([A-Z0-9\,]+)$/';
90 | if (!preg_match($pattern, $queryParams[$paramKey], $matches))
91 | {
92 | throw new Exception('Invalid ' . $paramKey . ' query key format', Exception::CODE_FORMAT_INVALID_QUERY_KEY);
93 | }
94 | $elementParts['Algorithm'] = $matches[1];
95 |
96 | foreach (self::basicQueryParamKeys() as $paramId) {
97 | $paramKey = self::checkParam($queryParams, $vendorKey, $paramId);
98 | $elementParts[$paramId] = $queryParams[$paramKey];
99 | }
100 | list($accessKeyId, $shortDate, $credentialScope) = explode('/', $elementParts['Credentials'], 3);
101 | $dateTime = Utils::parseLongDate($elementParts['Date']);
102 | return new AuthElements($elementParts, $accessKeyId, $shortDate, $credentialScope, $dateTime, self::checkHost($headerList), false);
103 | }
104 |
105 | private static function basicQueryParamKeys()
106 | {
107 | return [
108 | 'Credentials',
109 | 'Date',
110 | 'Expires',
111 | 'SignedHeaders',
112 | 'Signature'
113 | ];
114 | }
115 |
116 | /**
117 | * @param $queryParams
118 | * @param $vendorKey
119 | * @param $paramId
120 | * @return string
121 | * @throws Exception
122 | */
123 | private static function checkParam($queryParams, $vendorKey, $paramId)
124 | {
125 | $paramKey = 'X-' . $vendorKey . '-' . $paramId;
126 | if (!isset($queryParams[$paramKey])) {
127 | throw new Exception('Query key: ' . $paramKey . ' is missing', Exception::CODE_MISSING_QUERY_KEY_PARAM);
128 | }
129 | return $paramKey;
130 | }
131 |
132 | private static function checkHost($headerList)
133 | {
134 | if (!isset($headerList['host'])) {
135 | throw new Exception('The host header is missing', Exception::CODE_MISSING_HOST_HEADER);
136 | }
137 | return $headerList['host'];
138 | }
139 |
140 | public function validateDates(RequestHelper $helper, $clockSkew)
141 | {
142 | $shortDate = $this->dateTime->format('Ymd');
143 | if ($shortDate !== $this->getShortDate()) {
144 | throw new Exception('The credential date does not match with the request date', Exception::CODE_ARGUMENT_INVALID_DATE);
145 | }
146 |
147 | if (!$this->isInAcceptableInterval($helper->getTimeStamp(), Utils::getTimeStampOfDateTime($this->dateTime), $clockSkew)) {
148 | throw new Exception('The request date is not within the accepted time range', Exception::CODE_EXPIRED_TIME_RANGE);
149 | }
150 | }
151 |
152 | public function validateCredentials(RequestHelper $helper, $credentialScope)
153 | {
154 | if (!$this->checkCredentials($credentialScope)) {
155 | throw new Exception('The credential scope is invalid', Exception::CODE_ARGUMENT_INVALID_CREDENTIAL_SCOPE);
156 | }
157 | }
158 |
159 | private function checkCredentials($credentialScope)
160 | {
161 | return $this->credentialScope === $credentialScope;
162 | }
163 |
164 | public function validateSignature(RequestHelper $helper, Escher $escher, $keyDB, $vendorKey)
165 | {
166 | $secret = $this->lookupSecretKey($this->accessKeyId, $keyDB);
167 |
168 | $headers = $helper->getHeaderList();
169 | list($calculated, $canonicalizedRequest) = $escher->getSignature(
170 | $secret,
171 | $this->dateTime,
172 | $helper->getRequestMethod(),
173 | $this->stripAuthParams($helper, $vendorKey),
174 | $this->isFromHeaders ? $helper->getRequestBody() : Escher::UNSIGNED_PAYLOAD,
175 | $headers,
176 | $this->getSignedHeaders()
177 | );
178 |
179 | $provided = $this->getSignature();
180 | if ($calculated !== $provided) {
181 | throw new Exception('The signatures do not match', Exception::CODE_SIGNATURE_NOT_MATCH);
182 | }
183 | }
184 |
185 | private function lookupSecretKey($accessKeyId, $keyDB)
186 | {
187 | if (!isset($keyDB[$accessKeyId])) {
188 | throw new Exception('Invalid Escher key', Exception::CODE_ARGUMENT_INVALID_ESCHER_KEY);
189 | }
190 | return $keyDB[$accessKeyId];
191 | }
192 |
193 | public function validateHashAlgo()
194 | {
195 | if(!in_array(strtoupper($this->getAlgorithm()), ['SHA256','SHA512']))
196 | {
197 | throw new Exception('Only SHA256 and SHA512 hash algorithms are allowed', Exception::CODE_ARGUMENT_INVALID_HASH);
198 | }
199 | }
200 |
201 | /**
202 | * @param string $dateHeaderKey
203 | * @throws Exception
204 | */
205 | public function validateMandatorySignedHeaders($mandatorySignedHeaders)
206 | {
207 | $signedHeaders = $this->getSignedHeaders();
208 | if (!in_array('host', $signedHeaders)) {
209 | throw new Exception('The host header is not signed', Exception::CODE_NOT_SIGNED_HOST_HEADER);
210 | }
211 | foreach ($mandatorySignedHeaders as $headerName) {
212 | if ($this->isFromHeaders && !in_array(strtolower($headerName), $signedHeaders)) {
213 | throw new Exception('The ' . strtolower($headerName) . ' header is not signed', Exception::CODE_NOT_SIGNED_HEADER_PARAM);
214 | }
215 | }
216 | }
217 |
218 | public function getAccessKeyId()
219 | {
220 | return $this->accessKeyId;
221 | }
222 |
223 | public function getShortDate()
224 | {
225 | return $this->shortDate;
226 | }
227 |
228 | public function getSignedHeaders()
229 | {
230 | return explode(';', $this->elementParts['SignedHeaders']);
231 | }
232 |
233 | public function getSignature()
234 | {
235 | return $this->elementParts['Signature'];
236 | }
237 |
238 | public function getAlgorithm()
239 | {
240 | return $this->elementParts['Algorithm'];
241 | }
242 |
243 | public function getHost()
244 | {
245 | return $this->host;
246 | }
247 |
248 | /**
249 | * @param RequestHelper $helper
250 | * @param $vendorKey
251 | * @return string
252 | */
253 | private function stripAuthParams(RequestHelper $helper, $vendorKey)
254 | {
255 | $url = $helper->getCurrentUrl();
256 | $signaturePattern = "/(?P[?&])X-${vendorKey}-Signature=[a-fA-F0-9]{64}(?P&?)/";
257 |
258 | return preg_replace_callback($signaturePattern, [$this, 'handleStripAuthParamMatches'], $url);
259 | }
260 |
261 | private function getExpires()
262 | {
263 | return $this->isFromHeaders ? 0 : $this->elementParts['Expires'];
264 | }
265 |
266 | private function isInAcceptableInterval($currentTimeStamp, $requestTimeStamp, $clockSkew)
267 | {
268 | return ($requestTimeStamp - $clockSkew <= $currentTimeStamp)
269 | && ($currentTimeStamp <= $requestTimeStamp + $this->getExpires() + $clockSkew);
270 | }
271 |
272 | public function getCredentialScope()
273 | {
274 | return $this->credentialScope;
275 | }
276 |
277 | public function getDateTime()
278 | {
279 | return $this->dateTime;
280 | }
281 |
282 | private function handleStripAuthParamMatches($matches) {
283 | return (!empty($matches['suffix']) || $matches['prefix'] === '?')
284 | ? $matches['prefix']
285 | : $matches['suffix'];
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/src/Escher/Escher.php:
--------------------------------------------------------------------------------
1 | credentialScope = $credentialScope;
38 | }
39 |
40 | public static function create($credentialScope)
41 | {
42 | return new Escher($credentialScope);
43 | }
44 |
45 | /**
46 | * @return DateTime
47 | * @throws \Exception
48 | */
49 | private static function now()
50 | {
51 | return new DateTime('now', new DateTimeZone('GMT'));
52 | }
53 |
54 | /**
55 | * @param $keyDB
56 | * @param array|null $serverVars
57 | * @param null $requestBody
58 | * @return mixed
59 | * @throws Exception
60 | */
61 | public function authenticate($keyDB, array $serverVars = null, $requestBody = null, $mandatorySignedHeaders = [])
62 | {
63 | $serverVars = null === $serverVars ? $_SERVER : $serverVars;
64 | $requestBody = null === $requestBody ? $this->fetchRequestBodyFor($serverVars['REQUEST_METHOD']) : $requestBody;
65 |
66 | $algoPrefix = $this->algoPrefix;
67 | $vendorKey = $this->vendorKey;
68 | $helper = new RequestHelper($serverVars, $requestBody, $this->authHeaderKey, $this->dateHeaderKey);
69 |
70 | if (!in_array(strtolower($helper->getRequestMethod()), ['get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'])) {
71 | throw new Exception('The request method is invalid');
72 | }
73 | if (!is_array($mandatorySignedHeaders)) {
74 | throw new Exception('The mandatorySignedHeaders parameter must be undefined or array of strings');
75 | }
76 | foreach ($mandatorySignedHeaders as $headerName) {
77 | if (!is_string($headerName)) {
78 | throw new Exception('The mandatorySignedHeaders parameter must be undefined or array of strings');
79 | }
80 | }
81 |
82 | $authElements = $helper->getAuthElements($this->vendorKey, $algoPrefix);
83 |
84 | $mandatorySignedHeaders[] = $this->dateHeaderKey;
85 | $authElements->validateMandatorySignedHeaders($mandatorySignedHeaders);
86 | $authElements->validateHashAlgo();
87 | $authElements->validateDates($helper, $this->clockSkew);
88 | $authElements->validateCredentials($helper, $this->credentialScope);
89 | $authElements->validateSignature($helper, $this, $keyDB, $vendorKey);
90 | return $authElements->getAccessKeyId();
91 | }
92 |
93 | public function presignUrl($accessKeyId, $secretKey, $url, $expires = Escher::DEFAULT_EXPIRES, DateTime $date = null)
94 | {
95 | $date = $date ?: self::now();
96 | $url = $this->appendSigningParams($accessKeyId, $url, $date, $expires);
97 |
98 | list($host, $port, $path, $query) = $this->parseUrl($url);
99 | $portInOriginalUrl = $port && strpos($url, ':' . $port) !== false;
100 | $portInParsedHost = $port && strpos($host, ':' . $port) !== false;
101 | if ($portInOriginalUrl && !$portInParsedHost) {
102 | $host .= ':' . $port;
103 | }
104 |
105 | list($signature) = $this->calculateSignature(
106 | $secretKey,
107 | $date,
108 | 'GET',
109 | $path,
110 | $query,
111 | self::UNSIGNED_PAYLOAD,
112 | ['host' => $host],
113 | ['host']
114 | );
115 | $url = $this->addGetParameter($url, $this->generateParamName('Signature'), $signature);
116 |
117 | return $url;
118 | }
119 |
120 | public function signRequest($accessKeyId, $secretKey, $method, $url, $requestBody, $headerList = [], $headersToSign = [], DateTime $date = null)
121 | {
122 | if (!in_array(strtolower($method), ['get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'])) {
123 | throw new Exception('The request method is invalid');
124 | }
125 | if (!$accessKeyId || !$secretKey) {
126 | throw new Exception('Invalid Escher key');
127 | }
128 |
129 | $date = $date ?: self::now();
130 | list($host, , $path, $query) = $this->parseUrl($url);
131 | list($headerList, $headersToSign) = $this->addMandatoryHeaders(
132 | $headerList, $headersToSign, $this->dateHeaderKey, $date, $host
133 | );
134 |
135 | return $headerList + $this->generateAuthHeader(
136 | $secretKey,
137 | $accessKeyId,
138 | $this->authHeaderKey,
139 | $date,
140 | $method,
141 | $path,
142 | $query,
143 | $requestBody,
144 | $headerList,
145 | $headersToSign
146 | );
147 | }
148 |
149 | private function appendSigningParams($accessKeyId, $url, $date, $expires)
150 | {
151 | $signingParams = [
152 | 'Algorithm' => $this->algoPrefix. '-HMAC-' . $this->hashAlgo,
153 | 'Credentials' => $accessKeyId . '/' . $this->fullCredentialScope($date),
154 | 'Date' => $this->toLongDate($date),
155 | 'Expires' => $expires,
156 | 'SignedHeaders' => 'host',
157 | ];
158 | foreach ($signingParams as $param => $value)
159 | {
160 | $url = $this->addGetParameter($url, $this->generateParamName($param), $value);
161 | }
162 | return $url;
163 | }
164 |
165 | private function generateParamName($param)
166 | {
167 | return 'X-' . $this->vendorKey . '-' . $param;
168 | }
169 |
170 | public function getSignature($secretKey, DateTime $date, $method, $url, $requestBody, $headerList, $signedHeaders)
171 | {
172 | list(, , $path, $query) = $this->parseUrl($url);
173 | return $this->calculateSignature($secretKey, $date, $method, $path, $query, $requestBody, $headerList, $signedHeaders);
174 | }
175 |
176 | private function parseUrl($url)
177 | {
178 | $urlParts = parse_url(str_replace('#', '%23', $url));
179 | $defaultPort = $urlParts['scheme'] === 'http' ? 80 : 443;
180 | $port = isset($urlParts['port']) ? intval($urlParts['port']) : null;
181 | $host = $urlParts['host'] . ($port && $port !== $defaultPort ? ':' . $port : '');
182 | $path = $urlParts['path'] ?? null;
183 | $query = $urlParts['query'] ?? '';
184 | return [$host, $port, $path, $query];
185 | }
186 |
187 | private function toLongDate(DateTime $date)
188 | {
189 | return $date->format(self::LONG_DATE);
190 | }
191 |
192 | private function toHeaderDate(DateTime $date)
193 | {
194 | return str_replace(' +0000', ' GMT', $date->format('r'));
195 | }
196 |
197 | private function addGetParameter($url, $key, $value)
198 | {
199 | $glue = '?';
200 | if (strpos($url, '?') !== false) {
201 | $glue = '&';
202 | }
203 |
204 | $fragmentPosition = strpos($url, '#');
205 | if ($fragmentPosition === false) {
206 | return $url . $glue . $key . '=' . urlencode($value);
207 | }
208 |
209 | return substr_replace($url, ($glue . $key . '=' . urlencode($value)), $fragmentPosition, 0);
210 | }
211 |
212 | /**
213 | * @param $date
214 | * @return string
215 | */
216 | private function fullCredentialScope(DateTime $date)
217 | {
218 | return $date->format(self::SHORT_DATE) . '/' . $this->credentialScope;
219 | }
220 |
221 | private function generateAuthHeader($secretKey, $accessKeyId, $authHeaderKey, $date, $method, $path, $query, $requestBody, array $headerList, array $headersToSign)
222 | {
223 | list($signature) = $this->calculateSignature($secretKey, $date, $method, $path, $query, $requestBody, $headerList, $headersToSign);
224 | $authHeaderValue = Signer::createAuthHeader(
225 | $signature,
226 | $this->fullCredentialScope($date),
227 | implode(';', $headersToSign),
228 | $this->hashAlgo,
229 | $this->algoPrefix,
230 | $accessKeyId
231 | );
232 | return [strtolower($authHeaderKey) => $authHeaderValue];
233 | }
234 |
235 | private function calculateSignature($secretKey, $date, $method, $path, $query, $requestBody, array $headerList, array $headersToSign)
236 | {
237 | $hashAlgo = $this->hashAlgo;
238 | $algoPrefix = $this->algoPrefix;
239 | $requestUri = $path . ($query ? '?' . $query : '');
240 | // canonicalization works with raw headers
241 | $rawHeaderLines = [];
242 | foreach ($headerList as $headerKey => $headerValue) {
243 | $rawHeaderLines[] = $headerKey . ':' . $headerValue;
244 | }
245 | $canonicalizedRequest = RequestCanonicalizer::canonicalize(
246 | $method,
247 | $requestUri,
248 | $requestBody,
249 | implode("\n", $rawHeaderLines),
250 | $headersToSign,
251 | $hashAlgo
252 | );
253 | $this->debugInfo['canonicalizedRequest'] = $canonicalizedRequest;
254 |
255 | $stringToSign = Signer::createStringToSign(
256 | $this->credentialScope,
257 | $canonicalizedRequest,
258 | $date,
259 | $hashAlgo,
260 | $algoPrefix
261 | );
262 | $this->debugInfo['stringToSign'] = $stringToSign;
263 |
264 | $signerKey = Signer::calculateSigningKey(
265 | $secretKey,
266 | $this->fullCredentialScope($date),
267 | $hashAlgo,
268 | $algoPrefix
269 | );
270 |
271 | $signature = Signer::createSignature(
272 | $stringToSign,
273 | $signerKey,
274 | $hashAlgo
275 | );
276 |
277 | return [$signature, $canonicalizedRequest];
278 | }
279 |
280 | /**
281 | * @param $headerList
282 | * @param $headersToSign
283 | * @param $dateHeaderKey
284 | * @param $date
285 | * @param $host
286 | * @return array
287 | */
288 | private function addMandatoryHeaders($headerList, $headersToSign, $dateHeaderKey, $date, $host)
289 | {
290 | $dateHeaderKey = strtolower($dateHeaderKey);
291 | $mandatoryHeaders = [
292 | $dateHeaderKey => $dateHeaderKey === 'date' ? $this->toHeaderDate($date) : $this->toLongDate($date),
293 | 'host' => $host,
294 | ];
295 | $headerList = Utils::keysToLower($headerList) + $mandatoryHeaders;
296 | $headersToSign = array_unique(array_merge(array_map('strtolower', $headersToSign), array_keys($mandatoryHeaders)));
297 | sort($headersToSign);
298 | return [$headerList, $headersToSign];
299 | }
300 |
301 | /**
302 | * php://input may contain data even though the request body is empty, e.g. in GET requests
303 | *
304 | * @param string
305 | * @return string
306 | */
307 | private function fetchRequestBodyFor($method)
308 | {
309 | return in_array($method, ['PUT', 'POST', 'PATCH']) ? file_get_contents('php://input') : '';
310 | }
311 |
312 | /**
313 | * @param $clockSkew
314 | * @return Escher
315 | */
316 | public function setClockSkew($clockSkew)
317 | {
318 | $this->clockSkew = $clockSkew;
319 | return $this;
320 | }
321 |
322 | /**
323 | * @param $hashAlgo
324 | * @return Escher
325 | */
326 | public function setHashAlgo($hashAlgo)
327 | {
328 | $this->hashAlgo = strtoupper($hashAlgo);
329 | return $this;
330 | }
331 |
332 | /**
333 | * @param $algoPrefix
334 | * @return Escher
335 | */
336 | public function setAlgoPrefix($algoPrefix)
337 | {
338 | $this->algoPrefix = strtoupper($algoPrefix);
339 | return $this;
340 | }
341 |
342 | /**
343 | * @param $vendorKey
344 | * @return Escher
345 | */
346 | public function setVendorKey($vendorKey)
347 | {
348 | $this->vendorKey = $vendorKey;
349 | return $this;
350 | }
351 |
352 | /**
353 | * @param $authHeaderKey
354 | * @return Escher
355 | */
356 | public function setAuthHeaderKey($authHeaderKey)
357 | {
358 | $this->authHeaderKey = $authHeaderKey;
359 | return $this;
360 | }
361 |
362 | /**
363 | * @param $dateHeaderKey
364 | * @return Escher
365 | */
366 | public function setDateHeaderKey($dateHeaderKey)
367 | {
368 | $this->dateHeaderKey = $dateHeaderKey;
369 | return $this;
370 | }
371 | }
372 |
--------------------------------------------------------------------------------
/src/Escher/Exception.php:
--------------------------------------------------------------------------------
1 | credentialScope = $credentialScope;
17 | $this->escherKey = $escherKey;
18 | $this->escherSecret = $escherSecret;
19 | $this->keyDB = $keyDB;
20 | }
21 |
22 | /**
23 | * @return Escher
24 | */
25 | public function createEscher()
26 | {
27 | return Escher::create($this->credentialScope)
28 | ->setAlgoPrefix('EMS')
29 | ->setVendorKey('EMS')
30 | ->setAuthHeaderKey('X-Ems-Auth')
31 | ->setDateHeaderKey('X-Ems-Date');
32 | }
33 |
34 |
35 | /**
36 | * @return string
37 | */
38 | public function getEscherKey()
39 | {
40 | return $this->escherKey;
41 | }
42 |
43 |
44 | /**
45 | * @return string
46 | */
47 | public function getEscherSecret()
48 | {
49 | return $this->escherSecret;
50 | }
51 |
52 | /**
53 | * @return ArrayAccess|array
54 | */
55 | public function getKeyDB()
56 | {
57 | return $this->keyDB;
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/src/Escher/RequestCanonicalizer.php:
--------------------------------------------------------------------------------
1 | $key)
57 | {
58 | array_splice($path, $key - ($keypos * 2 + 1), 2);
59 | }
60 |
61 | $path = implode('/', $path);
62 | $path = str_replace(['./', '//'], ['', '/'], $path);
63 |
64 | if (empty($path)) {
65 | return '/';
66 | }
67 | return $path;
68 | }
69 |
70 | /**
71 | * @param $rawHeaders
72 | * @param $headersToSign
73 | * @return array
74 | */
75 | private static function canonicalizeHeaders($rawHeaders, array $headersToSign)
76 | {
77 | $result = [];
78 | foreach (explode("\n", $rawHeaders) as $header) {
79 | // TODO: add multiline header handling
80 | list ($key, $value) = explode(':', $header, 2);
81 | $lowerKey = strtolower($key);
82 | $trimmedValue = self::nomalizeHeaderValue($value);
83 | if (!in_array($lowerKey, $headersToSign)) {
84 | continue;
85 | }
86 | if (isset($result[$lowerKey])) {
87 | $result[$lowerKey] .= ',' . $trimmedValue;
88 | } else {
89 | $result[$lowerKey] = $lowerKey . ':' . $trimmedValue;
90 | }
91 | }
92 | sort($result);
93 | return $result;
94 | }
95 |
96 | private static function rawUrlEncode($urlComponent)
97 | {
98 | return str_replace(['%21', '%2A'], ['!', '*'], rawurlencode($urlComponent));
99 | }
100 |
101 | /**
102 | * @param $value
103 | * @return string
104 | */
105 | private static function nomalizeHeaderValue($value)
106 | {
107 | $result = [];
108 | foreach (explode('"', trim($value)) as $index => $piece) {
109 | $result[] = $index % 2 === 1 ? $piece : preg_replace('/\s+/', ' ', $piece);
110 | }
111 | return implode('"', $result);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Escher/RequestHelper.php:
--------------------------------------------------------------------------------
1 | serverVars = $serverVars;
16 | $this->requestBody = $requestBody;
17 | $this->authHeaderKey = $authHeaderKey;
18 | $this->dateHeaderKey = $dateHeaderKey;
19 | }
20 |
21 | public function getRequestMethod()
22 | {
23 | return $this->serverVars['REQUEST_METHOD'];
24 | }
25 |
26 | public function getRequestBody()
27 | {
28 | return $this->requestBody;
29 | }
30 |
31 | public function getAuthElements($vendorKey, $algoPrefix)
32 | {
33 | $headerList = Utils::keysToLower($this->getHeaderList());
34 | $queryParams = $this->getQueryParams();
35 | if (isset($headerList[strtolower($this->authHeaderKey)])) {
36 | return AuthElements::parseFromHeaders($headerList, $this->authHeaderKey, $this->dateHeaderKey, $algoPrefix);
37 | }
38 | if($this->getRequestMethod() === 'GET' && isset($queryParams[$this->paramKey($vendorKey, 'Signature')])) {
39 | return AuthElements::parseFromQuery($headerList, $queryParams, $vendorKey, $algoPrefix);
40 | }
41 | throw new Exception('The authorization header is missing', Exception::CODE_MISSING_AUTH);
42 | }
43 |
44 | public function getTimeStamp()
45 | {
46 | return $this->serverVars['REQUEST_TIME'];
47 | }
48 |
49 | public function getHeaderList()
50 | {
51 | $headerList = $this->process($this->serverVars);
52 | $headerList['content-type'] = $this->getContentType();
53 |
54 | if (isset($headerList['host'])) {
55 | if (strpos($headerList['host'], ':') === false) {
56 | $host = $headerList['host'];
57 | $port = null;
58 | } else {
59 | list($host, $port) = explode(':', $headerList['host'], 2);
60 | }
61 | $headerList['host'] = $this->normalizeHost($host, $port);
62 | }
63 |
64 | return $headerList;
65 | }
66 |
67 | public function getCurrentUrl()
68 | {
69 | $scheme = (array_key_exists('HTTPS', $this->serverVars) && $this->serverVars['HTTPS'] == 'on') ? 'https' : 'http';
70 | $host = $this->getServerHost();
71 | return "$scheme://$host" . $this->serverVars['REQUEST_URI'];
72 | }
73 |
74 | private function process(array $serverVars)
75 | {
76 | $headerList = [];
77 | foreach ($serverVars as $key => $value) {
78 | if (strpos($key, 'HTTP_') === 0) {
79 | $headerList[strtolower(str_replace('_', '-', substr($key, 5)))] = $value;
80 | }
81 | }
82 | return $headerList;
83 | }
84 |
85 | private function getContentType()
86 | {
87 | return isset($this->serverVars['CONTENT_TYPE']) ? $this->serverVars['CONTENT_TYPE'] : '';
88 | }
89 |
90 | public function getServerHost()
91 | {
92 | return $this->normalizeHost($this->serverVars['SERVER_NAME'], $this->serverVars['SERVER_PORT']);
93 | }
94 |
95 | /**
96 | * @param $vendorKey
97 | * @param $paramId
98 | * @return string
99 | */
100 | private function paramKey($vendorKey, $paramId)
101 | {
102 | return 'X-' . $vendorKey . '-' . $paramId;
103 | }
104 |
105 | public function getQueryParams()
106 | {
107 | list(, $queryString) = array_pad(explode('?', $this->serverVars['REQUEST_URI'], 2), 2, '');
108 | parse_str($queryString, $result);
109 | return $result;
110 | }
111 |
112 | private function normalizeHost($host, $port)
113 | {
114 | if (is_null($port) || $this->isDefaultPort($port)) {
115 | return $host;
116 | }
117 |
118 | return $host . ':' . $port;
119 | }
120 |
121 | private function isDefaultPort($port)
122 | {
123 | $defaultPort = isset($this->serverVars['HTTPS']) && $this->serverVars['HTTPS'] === 'on' ? '443' : '80';
124 | return $port == $defaultPort;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Escher/Signer.php:
--------------------------------------------------------------------------------
1 | setTimezone(new DateTimeZone('GMT'));
14 | $formattedDate = $date->format(Escher::LONG_DATE);
15 | $scope = substr($formattedDate,0, 8) . '/' . $credentialScope;
16 | $lines = [];
17 | $lines[] = $algoPrefix . '-HMAC-' . strtoupper($hashAlgo);
18 | $lines[] = $formattedDate;
19 | $lines[] = $scope;
20 | $lines[] = hash($hashAlgo, $canonicalRequestString);
21 | return implode("\n", $lines);
22 | }
23 |
24 | public static function calculateSigningKey($secret, $credentialScope, $hashAlgo, $algoPrefix)
25 | {
26 | $key = $algoPrefix . $secret;
27 | $credentials = explode('/', $credentialScope);
28 | foreach ($credentials as $data) {
29 | $key = hash_hmac($hashAlgo, $data, $key, true);
30 | }
31 | return $key;
32 | }
33 |
34 | public static function createAuthHeader($signature, $credentialScope, $signedHeaders, $hashAlgo, $algoPrefix, $accessKey)
35 | {
36 | return $algoPrefix . '-HMAC-' . strtoupper($hashAlgo)
37 | . ' Credential='
38 | . $accessKey . '/'
39 | . $credentialScope
40 | . ', SignedHeaders=' . $signedHeaders
41 | . ', Signature=' . $signature;
42 | }
43 |
44 | public static function createSignature($stringToSign, $signerKey, $hashAlgo)
45 | {
46 | return hash_hmac($hashAlgo, $stringToSign, $signerKey);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Escher/Utils.php:
--------------------------------------------------------------------------------
1 | format('U');
37 | }
38 | return $dateTime->getTimestamp();
39 | }
40 |
41 | /**
42 | * @return bool
43 | */
44 | protected static function advancedDateTimeFunctionsAvailable()
45 | {
46 | return version_compare(PHP_VERSION, '5.3.0') !== -1;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/Escher/Test/EndToEnd/CentralTest.php:
--------------------------------------------------------------------------------
1 | 'PHP does not handle multiple headers with the same name directly, the server before PHP does the conversion into the comma separated list',
14 | 'emarsys_testsuite/signrequest/get-header-key-duplicate' => 'PHP does not handle multiple headers with the same name directly, the server before PHP does the conversion into the comma separated list',
15 | 'test_cases/signrequest/error-invalid-request-url' => 'Not applicable for the PHP implementation',
16 | 'test_cases/authenticate/error-post-body-null' => 'Not applicable for the PHP implementation',
17 | 'test_cases/authenticate/error-invalid-request-url' => 'Not applicable for the PHP implementation',
18 | ];
19 |
20 | public function signRequestTestCases()
21 | {
22 | $data = [];
23 | foreach (JsonTestCase::getTestCases('signrequest') as $testCase) {
24 | $data["{$testCase->suite}/{$testCase->type}/{$testCase->name}"] = [$testCase];
25 | }
26 | return $data;
27 | }
28 |
29 | public function authenticateTestCases()
30 | {
31 | $data = [];
32 | foreach (JsonTestCase::getTestCases('authenticate') as $testCase) {
33 | $data["{$testCase->suite}/{$testCase->type}/{$testCase->name}"] = [$testCase];
34 | }
35 | return $data;
36 | }
37 |
38 | public function presignUrlTestCases()
39 | {
40 | $data = [];
41 | foreach (JsonTestCase::getTestCases('presignurl') as $testCase) {
42 | $data["{$testCase->suite}/{$testCase->type}/{$testCase->name}"] = [$testCase];
43 | }
44 | return $data;
45 | }
46 |
47 | /**
48 | * @test
49 | * @dataProvider signRequestTestCases
50 | */
51 | public function signRequestTests(JsonTestCase $testCase)
52 | {
53 | if (array_key_exists("{$testCase->suite}/{$testCase->type}/{$testCase->name}", self::$ignoredTestCases)) {
54 | $this->markTestSkipped(self::$ignoredTestCases["{$testCase->suite}/{$testCase->type}/{$testCase->name}"]);
55 | }
56 |
57 | $escher = Escher::create($testCase->getCredentialScope())
58 | ->setAlgoPrefix($testCase->getAlgoPrefix())
59 | ->setVendorKey($testCase->getVendorKey())
60 | ->setHashAlgo($testCase->getHashAlgo())
61 | ->setAuthHeaderKey($testCase->getAuthHeaderName())
62 | ->setDateHeaderKey($testCase->getDateHeaderName());
63 |
64 | $request = $testCase->getRequest();
65 |
66 | try {
67 | $host = $request['headers']['Host'] ?? $request['headers']['host'];
68 | $signedHeaders = $escher->signRequest(
69 | $testCase->getApiKey(),
70 | $testCase->getApiSecret(),
71 | $request['method'],
72 | 'https://' . $host . $request['url'],
73 | $request['body'],
74 | $request['headers'],
75 | $testCase->getHeadersToSign(),
76 | $testCase->getCurrentTime()
77 | );
78 |
79 | if ($testCase->hasExpectedCanonicalizedRequest()) {
80 | $this->assertEquals($testCase->getExpectedCanonicalizedRequest(), $escher->debugInfo['canonicalizedRequest']);
81 | }
82 | if ($testCase->hasExpectedStringToSign()) {
83 | $this->assertEquals($testCase->getExpectedStringToSign(), $escher->debugInfo['stringToSign']);
84 | }
85 | if ($testCase->hasExpectedHeaders()) {
86 | $this->assertEquals($testCase->getExpectedHeaders(), $signedHeaders);
87 | } else {
88 | $this->fail('no request in expected');
89 | }
90 | } catch (Exception $e) {
91 | if ($testCase->hasExpectedError()) {
92 | $this->assertEquals($testCase->getExpectedError(), $e->getMessage());
93 | } else {
94 | throw $e;
95 | }
96 | }
97 | }
98 |
99 | /**
100 | * @test
101 | * @dataProvider authenticateTestCases
102 | */
103 | public function authenticateTests(JsonTestCase $testCase)
104 | {
105 | if (array_key_exists("{$testCase->suite}/{$testCase->type}/{$testCase->name}", self::$ignoredTestCases)) {
106 | $this->markTestSkipped(self::$ignoredTestCases["{$testCase->suite}/{$testCase->type}/{$testCase->name}"]);
107 | }
108 |
109 | $escher = Escher::create($testCase->getCredentialScope())
110 | ->setAlgoPrefix($testCase->getAlgoPrefix())
111 | ->setVendorKey($testCase->getVendorKey())
112 | ->setAuthHeaderKey($testCase->getAuthHeaderName())
113 | ->setDateHeaderKey($testCase->getDateHeaderName());
114 |
115 | $request = $testCase->getRequest();
116 | $serverVars = [
117 | 'REQUEST_METHOD' => $request['method'],
118 | 'REQUEST_URI' => $request['url'],
119 | 'REQUEST_TIME' => $testCase->getCurrentTime()->format('U'),
120 | 'HTTPS' => 'on',
121 | 'SERVER_PORT' => '443',
122 | 'SERVER_NAME' => $request['headers']['Host'] ?? $request['headers']['host'] ?? null,
123 | ];
124 | foreach ($request['headers'] as $k => $v) {
125 | $serverVars['HTTP_' . str_replace('-', '_', strtoupper($k))] = $v;
126 | }
127 |
128 | try {
129 | $apiKey = $escher->authenticate($testCase->getKeyDb(), $serverVars, $request['body'] ?? null, $testCase->getMandatorySignedHeaders());
130 | if ($testCase->hasExpectedApiKey()) {
131 | $this->assertEquals($testCase->getExpectedApiKey(), $apiKey);
132 | } else {
133 | $this->fail('no apiKey in expected');
134 | }
135 | } catch (Exception $e) {
136 | if ($testCase->hasExpectedError()) {
137 | $this->assertEquals($testCase->getExpectedError(), $e->getMessage());
138 | } else {
139 | throw $e;
140 | }
141 | }
142 | }
143 |
144 | /**
145 | * @test
146 | * @dataProvider presignUrlTestCases
147 | */
148 | public function presignUrlTests(JsonTestCase $testCase)
149 | {
150 | if (array_key_exists("{$testCase->suite}/{$testCase->type}/{$testCase->name}", self::$ignoredTestCases)) {
151 | $this->markTestSkipped(self::$ignoredTestCases["{$testCase->suite}/{$testCase->type}/{$testCase->name}"]);
152 | }
153 |
154 | $escher = Escher::create($testCase->getCredentialScope())
155 | ->setAlgoPrefix($testCase->getAlgoPrefix())
156 | ->setVendorKey($testCase->getVendorKey())
157 | ->setAuthHeaderKey($testCase->getAuthHeaderName())
158 | ->setDateHeaderKey($testCase->getDateHeaderName());
159 |
160 | $request = $testCase->getRequest();
161 | $url = $escher->presignUrl(
162 | $testCase->getApiKey(),
163 | $testCase->getApiSecret(),
164 | $request['url'],
165 | $request['expires'],
166 | $testCase->getCurrentTime()
167 | );
168 |
169 | $this->assertEquals($testCase->getExpectedUrl(), $url);
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/test/Escher/Test/Helper/JsonTestCase.php:
--------------------------------------------------------------------------------
1 | [^/]+)/(?P[^-]+)-(?P[^.]+)\.json$#', \RegexIterator::GET_MATCH);
32 |
33 | $cases = [];
34 | foreach ($matches as $match) {
35 | if ($match['type'] !== $type) {
36 | continue;
37 | }
38 | if ($match['suite'] === '.conflict') {
39 | continue;
40 | }
41 | $cases[] = new JsonTestCase($match[0], $match['suite'], $match['type'], $match['name']);
42 | }
43 |
44 | return $cases;
45 | }
46 |
47 | public function __construct(string $path, string $suite, string $type, string $name)
48 | {
49 | $this->suite = $suite;
50 | $this->type = $type;
51 | $this->name = $name;
52 |
53 | $this->data = json_decode(file_get_contents($path), true);
54 | }
55 |
56 | public function getCredentialScope(): string
57 | {
58 | return $this->data['config']['credentialScope'];
59 | }
60 |
61 | public function getHeadersToSign(): array
62 | {
63 | return $this->data['headersToSign'];
64 | }
65 |
66 | public function getRequest(): array
67 | {
68 | $request = $this->data['request'];
69 | $request['headers'] = [];
70 | foreach ($this->data['request']['headers'] ?? [] as $h) {
71 | $request['headers'][$h[0]] = $h[1];
72 | }
73 |
74 | return $request;
75 | }
76 |
77 | public function hasExpectedCanonicalizedRequest(): bool
78 | {
79 | return array_key_exists('canonicalizedRequest', $this->data['expected']);
80 | }
81 |
82 | public function hasExpectedStringToSign(): bool
83 | {
84 | return array_key_exists('stringToSign', $this->data['expected']);
85 | }
86 |
87 | public function hasExpectedHeaders(): bool
88 | {
89 | return array_key_exists('request', $this->data['expected']);
90 | }
91 |
92 | public function hasExpectedApiKey(): bool
93 | {
94 | return array_key_exists('apiKey', $this->data['expected']);
95 | }
96 |
97 | public function hasExpectedError(): bool
98 | {
99 | return array_key_exists('error', $this->data['expected']);
100 | }
101 |
102 | public function getExpectedCanonicalizedRequest(): string
103 | {
104 | return $this->data['expected']['canonicalizedRequest'];
105 | }
106 |
107 | public function getExpectedStringToSign(): string
108 | {
109 | return $this->data['expected']['stringToSign'];
110 | }
111 |
112 | public function getExpectedHeaders(): array
113 | {
114 | $headers = [];
115 | foreach ($this->data['expected']['request']['headers'] ?: [] as $h) {
116 | $headers[strtolower($h[0])] = $h[1];
117 | }
118 | return $headers;
119 | }
120 |
121 | public function getExpectedUrl(): string
122 | {
123 | return $this->data['expected']['url'];
124 | }
125 |
126 | public function getExpectedApiKey(): string
127 | {
128 | return $this->data['expected']['apiKey'];
129 | }
130 |
131 | public function getExpectedError(): string
132 | {
133 | return $this->data['expected']['error'];
134 | }
135 |
136 | public function getMandatorySignedHeaders()
137 | {
138 | return $this->data['mandatorySignedHeaders'] ?? [];
139 | }
140 |
141 | public function getAlgoPrefix(): string
142 | {
143 | return $this->data['config']['algoPrefix'];
144 | }
145 |
146 | public function getVendorKey(): string
147 | {
148 | return $this->data['config']['vendorKey'];
149 | }
150 |
151 | public function getHashAlgo(): string
152 | {
153 | return $this->data['config']['hashAlgo'];
154 | }
155 |
156 | public function getAuthHeaderName(): ?string
157 | {
158 | return $this->data['config']['authHeaderName'] ?? null;
159 | }
160 |
161 | public function getDateHeaderName(): ?string
162 | {
163 | return $this->data['config']['dateHeaderName'] ?? null;
164 | }
165 |
166 | public function getApiKey(): string
167 | {
168 | return $this->data['config']['accessKeyId'];
169 | }
170 |
171 | public function getApiSecret(): ?string
172 | {
173 | return $this->data['config']['apiSecret'] ?? null;
174 | }
175 |
176 | public function getKeyDb(): array
177 | {
178 | $keyDb = [];
179 | foreach ($this->data['keyDb'] as $v) {
180 | $keyDb[$v[0]] = $v[1];
181 | }
182 | return $keyDb;
183 | }
184 |
185 | public function getCurrentTime(): DateTime
186 | {
187 | $timeFormats = ['Y-m-d\TH:i:s.000Z', 'Y-m-d\TH:i:s\Z', 'l, d M Y H:i:s \G\M\T'];
188 |
189 | foreach ($timeFormats as $format) {
190 | $currentTime = DateTime::createFromFormat($format, $this->data['config']['date'], new DateTimeZone('UTC'));
191 | if ($currentTime) {
192 | return $currentTime;
193 | }
194 | }
195 |
196 | throw new \Exception("Invalid time => {$this->data['config']['date']}");
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/test/Escher/Test/Helper/TestBase.php:
--------------------------------------------------------------------------------
1 | assertEquals($expected, $actual, $message);
17 | }
18 |
19 | protected function createEscher(string $credentialScope = 'us-east-1/host/aws4_request'): Escher
20 | {
21 | return Escher::create($credentialScope)
22 | ->setAlgoPrefix('EMS')
23 | ->setVendorKey('EMS')
24 | ->setAuthHeaderKey('X-Ems-Auth')
25 | ->setDateHeaderKey('X-Ems-Date');
26 | }
27 |
28 | protected function getDate(): DateTime
29 | {
30 | return new DateTime('2011/05/11 12:00:00', new DateTimeZone('UTC'));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/Escher/Test/Unit/AuthenticateRequestTest.php:
--------------------------------------------------------------------------------
1 | '20110909T233600Z',
21 | 'HTTP_X_EMS_AUTH' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
22 | 'REQUEST_TIME' => $this->strtotime('20110909T233600Z'),
23 | 'REQUEST_METHOD' => 'POST',
24 | 'HTTP_HOST' => 'iam.amazonaws.com',
25 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
26 | 'REQUEST_URI' => '/',
27 | 'HTTPS' => '',
28 | 'SERVER_PORT' => '80',
29 | 'SERVER_NAME' => 'iam.amazonaws.com',
30 | ];
31 | $keyDB = ['AKIDEXAMPLE' => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'];
32 | $accessKeyId = $this->createEscher('us-east-1/iam/aws4_request')
33 | ->authenticate($keyDB, $serverVars, 'Action=ListUsers&Version=2010-05-08');
34 | $this->assertEquals('AKIDEXAMPLE', $accessKeyId);
35 | }
36 |
37 | /**
38 | * @test
39 | * @dataProvider validPortProvider
40 | * @param $httpHost
41 | * @param $serverName
42 | * @param $serverPort
43 | * @param $https
44 | * @param $signature
45 | * @throws Exception
46 | */
47 | public function itShouldAuthenticateRequestRegardlessDefaultPortProvidedOrNot(
48 | $httpHost,
49 | $serverName,
50 | $serverPort,
51 | $https,
52 | $signature
53 | ) {
54 | $serverVars = [
55 | 'HTTP_X_EMS_DATE' => '20110909T233600Z',
56 | 'HTTP_X_EMS_AUTH' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=' . $signature,
57 | 'REQUEST_TIME' => $this->strtotime('20110909T233600Z'),
58 | 'REQUEST_METHOD' => 'POST',
59 | 'HTTP_HOST' => $httpHost,
60 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
61 | 'REQUEST_URI' => '/',
62 | 'HTTPS' => $https,
63 | 'SERVER_PORT' => $serverPort,
64 | 'SERVER_NAME' => $serverName,
65 | ];
66 | $keyDB = ['AKIDEXAMPLE' => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'];
67 | $accessKeyId = $this->createEscher('us-east-1/iam/aws4_request')
68 | ->authenticate($keyDB, $serverVars, 'Action=ListUsers&Version=2010-05-08');
69 | $this->assertEquals('AKIDEXAMPLE', $accessKeyId);
70 | }
71 |
72 | public function validPortProvider()
73 | {
74 | return [
75 | 'default http port not provided' => [
76 | 'iam.amazonaws.com',
77 | 'iam.amazonaws.com',
78 | '80',
79 | '',
80 | 'f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd'
81 | ],
82 | 'default http port provided' => [
83 | 'iam.amazonaws.com:80',
84 | 'iam.amazonaws.com',
85 | '80',
86 | '',
87 | 'f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd'
88 | ],
89 | 'default https port not provided' => [
90 | 'iam.amazonaws.com',
91 | 'iam.amazonaws.com',
92 | '443',
93 | 'on',
94 | 'f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd'
95 | ],
96 | 'default https port provided' => [
97 | 'iam.amazonaws.com:443',
98 | 'iam.amazonaws.com',
99 | '443',
100 | 'on',
101 | 'f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd'
102 | ],
103 | 'custom http port' => [
104 | 'iam.amazonaws.com:123',
105 | 'iam.amazonaws.com',
106 | '123',
107 | '',
108 | '9584a4a527986bbbead79b56523d50e1c8161155933a644674d0b2f2a0bce19a'
109 | ],
110 | 'custom https port' => [
111 | 'iam.amazonaws.com:123',
112 | 'iam.amazonaws.com',
113 | '123',
114 | 'on',
115 | '9584a4a527986bbbead79b56523d50e1c8161155933a644674d0b2f2a0bce19a'
116 | ],
117 | 'default http port as custom https port' => [
118 | 'iam.amazonaws.com:80',
119 | 'iam.amazonaws.com',
120 | '80',
121 | 'on',
122 | 'b5daefdecb7124f47fafad18549e18a1a9c5accc4216a146c919d0635eccc370'
123 | ],
124 | 'default https port as custom http port' => [
125 | 'iam.amazonaws.com:443',
126 | 'iam.amazonaws.com',
127 | '443',
128 | '',
129 | 'b36c465c5a6bb79e6c6ac666e9c3847d5c997e035321429b7c25777ea86af35c'
130 | ]
131 | ];
132 | }
133 |
134 | /**
135 | * @test
136 | * @dataProvider requestTamperingProvider
137 | * @param $tamperedKey
138 | * @param $tamperedValue
139 | * @param $expectedErrorMessage
140 | * @param $expectedErrorCode
141 | * @throws Exception
142 | */
143 | public function itShouldFailToValidateInvalidRequests(
144 | $tamperedKey,
145 | $tamperedValue,
146 | $expectedErrorMessage,
147 | $expectedErrorCode
148 | ) {
149 | $serverVars = [
150 | 'HTTP_X_EMS_DATE' => '20110909T233600Z',
151 | 'HTTP_X_EMS_AUTH' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
152 | 'REQUEST_TIME' => $this->strtotime('20110909T233600Z'),
153 | 'REQUEST_METHOD' => 'POST',
154 | 'HTTP_HOST' => 'iam.amazonaws.com',
155 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
156 | 'REQUEST_URI' => '/',
157 | 'HTTPS' => '',
158 | 'SERVER_PORT' => '80',
159 | 'SERVER_NAME' => 'iam.amazonaws.com',
160 | ];
161 |
162 | // replace server variable
163 | $serverVars[$tamperedKey] = $tamperedValue;
164 |
165 | $keyDB = ['AKIDEXAMPLE' => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'];
166 |
167 | try {
168 | $this->createEscher('us-east-1/iam/aws4_request')
169 | ->authenticate($keyDB, $serverVars, 'Action=ListUsers&Version=2010-05-08');
170 | $this->fail('Should fail to validate!');
171 | } catch (Exception $ex) {
172 | $this->assertStringStartsWith($expectedErrorMessage, $ex->getMessage());
173 | $this->assertEquals($expectedErrorCode, $ex->getCode());
174 | }
175 | }
176 |
177 | public function requestTamperingProvider()
178 | {
179 | return [
180 | 'wrong auth header' => [
181 | 'HTTP_X_EMS_AUTH',
182 | 'Malformed auth header',
183 | 'Could not parse auth header',
184 | 2002
185 | ],
186 | 'wrong date' => [
187 | 'HTTP_X_EMS_DATE',
188 | 'INVALIDDATE',
189 | 'Date header is invalid, the expected format is 20151104T092022Z',
190 | 2004
191 | ],
192 | 'invalid Escher key' => [
193 | 'HTTP_X_EMS_AUTH',
194 | 'EMS-HMAC-SHA256 Credential=FOOBAR/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
195 | 'Invalid Escher key',
196 | 3001
197 | ],
198 | 'wrong hash algo' => [
199 | 'HTTP_X_EMS_AUTH',
200 | 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
201 | 'Only SHA256 and SHA512 hash algorithms are allowed',
202 | 3002
203 | ],
204 | 'invalid credential' => [
205 | 'HTTP_X_EMS_AUTH',
206 | 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-2/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
207 | 'The credential scope is invalid',
208 | 3003
209 | ],
210 | 'host not signed' => [
211 | 'HTTP_X_EMS_AUTH',
212 | 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
213 | 'The host header is not signed',
214 | 4001
215 | ],
216 | 'date not signed' => [
217 | 'HTTP_X_EMS_AUTH',
218 | 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
219 | 'The x-ems-date header is not signed',
220 | 4002
221 | ],
222 | 'wrong request time' => [
223 | 'REQUEST_TIME',
224 | '20110909T113600Z',
225 | 'The request date is not within the accepted time range',
226 | 5001
227 | ],
228 | 'tampered signature' => [
229 | 'HTTP_X_EMS_AUTH',
230 | 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
231 | 'The signatures do not match',
232 | 6001
233 | ],
234 | ];
235 | }
236 |
237 | /**
238 | * @test
239 | * @throws Exception
240 | */
241 | public function itShouldValidateRequestUsingQueryString()
242 | {
243 | $serverVars = [
244 | 'REQUEST_TIME' => $this->strtotime('20110511T120000Z'),
245 | 'REQUEST_METHOD' => 'GET',
246 | 'HTTP_HOST' => 'example.com',
247 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
248 | 'REQUEST_URI' => '/something?foo=bar&baz=barbaz&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=th3K3y%2F20110511%2Fus-east-1%2Fhost%2Faws4_request&X-EMS-Date=20110511T120000Z&X-EMS-Expires=123456&X-EMS-SignedHeaders=host&X-EMS-Signature=fbc9dbb91670e84d04ad2ae7505f4f52ab3ff9e192b8233feeae57e9022c2b67',
249 | 'HTTPS' => '',
250 | 'SERVER_PORT' => '80',
251 | 'SERVER_NAME' => 'example.com',
252 | ];
253 | $keyDB = ['th3K3y' => 'very_secure'];
254 |
255 | $accessKeyId = $this->createEscher('us-east-1/host/aws4_request')->authenticate($keyDB, $serverVars, '');
256 | $this->assertEquals('th3K3y', $accessKeyId);
257 | }
258 |
259 | /**
260 | * @test
261 | * @throws Exception
262 | */
263 | public function itShouldValidatePresignedUrlRequestWithSpecialCharacters()
264 | {
265 | $serverVars = [
266 | 'REQUEST_TIME' => $this->strtotime('20150310T173248Z'),
267 | 'REQUEST_METHOD' => 'GET',
268 | 'HTTP_HOST' => 'service.example.com',
269 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
270 | 'REQUEST_URI' => '/login?id=12345678&domain=login.example.com&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name%3F&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=service_api_key%2F20150310%2Feu%2Fservice%2Fems_request&X-EMS-Date=20150310T173248Z&X-EMS-Expires=86400&X-EMS-SignedHeaders=host&X-EMS-Signature=661f2147c77b6784be5a60a8b842a96de6327653f1ed5d4305da43103c69a6f5',
271 | 'HTTPS' => 'on',
272 | 'SERVER_PORT' => '443',
273 | 'SERVER_NAME' => 'service.example.com',
274 | ];
275 | $keyDB = ['service_api_key' => 'service_secret'];
276 |
277 | $accessKeyId = $this->createEscher('eu/service/ems_request')->authenticate($keyDB, $serverVars);
278 | $this->assertEquals('service_api_key', $accessKeyId);
279 | }
280 |
281 | /**
282 | * @test
283 | */
284 | public function itShouldFailToValidateInvalidQueryStrings()
285 | {
286 | $this->expectException(Escher\Exception::class);
287 | $this->expectExceptionMessage('The signatures do not match');
288 | $this->expectExceptionCode(6001);
289 | $serverVars = [
290 | 'REQUEST_TIME' => $this->strtotime('20110511T120000Z'),
291 | 'REQUEST_METHOD' => 'GET',
292 | 'HTTP_HOST' => 'example.com',
293 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
294 | 'REQUEST_URI' => '/something?foo=bar&baz=barbaz&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=th3K3y%2F20110511%2Fus-east-1%2Fhost%2Faws4_request&X-EMS-Date=20110511T120000Z&X-EMS-Expires=' . PHP_INT_MAX . '&X-EMS-SignedHeaders=host&X-EMS-Signature=fbc9dbb91670e84d04ad2ae7505f4f52ab3ff9e192b8233feeae57e9022c2b67',
295 | 'HTTPS' => '',
296 | 'SERVER_PORT' => '80',
297 | 'SERVER_NAME' => 'example.com',
298 | ];
299 |
300 | $keyDB = ['th3K3y' => 'very_secure'];
301 | $this->createEscher('us-east-1/host/aws4_request')->authenticate($keyDB, $serverVars, '');
302 | }
303 |
304 | /**
305 | * @test
306 | * @throws Exception
307 | */
308 | public function itShouldValidatePresignedUrlRequestWithUnindexedArray()
309 | {
310 | $serverVars = [
311 | 'REQUEST_TIME' => $this->strtotime('20150310T173248Z'),
312 | 'REQUEST_METHOD' => 'GET',
313 | 'HTTP_HOST' => 'service.example.com',
314 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
315 | 'REQUEST_URI' => '/login?id=12345678&domain=login.example.com&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name¶m1%5B%5D=1¶m1%5B%5D=2%3F&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=service_api_key%2F20150310%2Feu%2Fservice%2Fems_request&X-EMS-Date=20150310T173248Z&X-EMS-Expires=86400&X-EMS-SignedHeaders=host&X-EMS-Signature=ddb1e6479f28752c23a2a7f12fa54d3f21c4b36b8247e88e5992975a10ba616c',
316 | 'HTTPS' => 'on',
317 | 'SERVER_PORT' => '443',
318 | 'SERVER_NAME' => 'service.example.com',
319 | ];
320 | $keyDB = ['service_api_key' => 'service_secret'];
321 |
322 | $accessKeyId = $this->createEscher('eu/service/ems_request')->authenticate($keyDB, $serverVars);
323 | $this->assertEquals('service_api_key', $accessKeyId);
324 | }
325 |
326 | /**
327 | * @test
328 | * @throws Exception
329 | */
330 | public function itShouldValidatePresignedUrlRequestWithIndexedArray()
331 | {
332 | $serverVars = [
333 | 'REQUEST_TIME' => $this->strtotime('20150310T173248Z'),
334 | 'REQUEST_METHOD' => 'GET',
335 | 'HTTP_HOST' => 'service.example.com',
336 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
337 | 'REQUEST_URI' => '/login?id=12345678&domain=login.example.com&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name¶m1%5B0%5D=1¶m1%5B1%5D=2%3F&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=service_api_key%2F20150310%2Feu%2Fservice%2Fems_request&X-EMS-Date=20150310T173248Z&X-EMS-Expires=86400&X-EMS-SignedHeaders=host&X-EMS-Signature=196bc22e36ea13d2bfe59c3fb42fbf67a09ec501a79924284d9281d7d8c773ce',
338 | 'HTTPS' => 'on',
339 | 'SERVER_PORT' => '443',
340 | 'SERVER_NAME' => 'service.example.com',
341 | ];
342 | $keyDB = ['service_api_key' => 'service_secret'];
343 |
344 | $accessKeyId = $this->createEscher('eu/service/ems_request')->authenticate($keyDB, $serverVars);
345 | $this->assertEquals('service_api_key', $accessKeyId);
346 | }
347 |
348 | /**
349 | * @test
350 | * @throws Exception
351 | */
352 | public function itShouldValidatePresignedUrlIfSignatureIsTheFirstParam()
353 | {
354 | $serverVars = [
355 | 'REQUEST_TIME' => $this->strtotime('20150310T173248Z'),
356 | 'REQUEST_METHOD' => 'GET',
357 | 'HTTP_HOST' => 'service.example.com',
358 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
359 | 'REQUEST_URI' => '/login?X-EMS-Signature=196bc22e36ea13d2bfe59c3fb42fbf67a09ec501a79924284d9281d7d8c773ce&id=12345678&domain=login.example.com&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name¶m1%5B0%5D=1¶m1%5B1%5D=2%3F&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=service_api_key%2F20150310%2Feu%2Fservice%2Fems_request&X-EMS-Date=20150310T173248Z&X-EMS-Expires=86400&X-EMS-SignedHeaders=host',
360 | 'HTTPS' => 'on',
361 | 'SERVER_PORT' => '443',
362 | 'SERVER_NAME' => 'service.example.com',
363 | ];
364 | $keyDB = ['service_api_key' => 'service_secret'];
365 |
366 | $accessKeyId = $this->createEscher('eu/service/ems_request')->authenticate($keyDB, $serverVars);
367 | $this->assertEquals('service_api_key', $accessKeyId);
368 | }
369 |
370 | /**
371 | * @test
372 | * @throws Exception
373 | */
374 | public function itShouldValidatePresignedUrlIfSignatureIsInTheMiddleOfTheQueryString()
375 | {
376 | $serverVars = [
377 | 'REQUEST_TIME' => $this->strtotime('20150310T173248Z'),
378 | 'REQUEST_METHOD' => 'GET',
379 | 'HTTP_HOST' => 'service.example.com',
380 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
381 | 'REQUEST_URI' => '/login?id=12345678&domain=login.example.com&X-EMS-Signature=196bc22e36ea13d2bfe59c3fb42fbf67a09ec501a79924284d9281d7d8c773ce&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name¶m1%5B0%5D=1¶m1%5B1%5D=2%3F&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=service_api_key%2F20150310%2Feu%2Fservice%2Fems_request&X-EMS-Date=20150310T173248Z&X-EMS-Expires=86400&X-EMS-SignedHeaders=host',
382 | 'HTTPS' => 'on',
383 | 'SERVER_PORT' => '443',
384 | 'SERVER_NAME' => 'service.example.com',
385 | ];
386 | $keyDB = ['service_api_key' => 'service_secret'];
387 |
388 | $accessKeyId = $this->createEscher('eu/service/ems_request')->authenticate($keyDB, $serverVars);
389 | $this->assertEquals('service_api_key', $accessKeyId);
390 | }
391 |
392 | /**
393 | * @param $dateString
394 | * @return string
395 | * @throws Exception
396 | */
397 | private function strtotime($dateString)
398 | {
399 | return Utils::parseLongDate($dateString)->format('U');
400 | }
401 | }
402 |
403 |
--------------------------------------------------------------------------------
/test/Escher/Test/Unit/InternalTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
27 | "98f1d889fec4f4421adc522bab0ce1f82e6929c262ed15e5a94c90efd1e3b0e7",
28 | bin2hex($actualSigningKey)
29 | );
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function itShouldCollectBodyAndHeadersFromServerVariables()
36 | {
37 | $serverVars = [
38 | 'REQUEST_TIME' => time(),
39 | 'REQUEST_METHOD' => 'GET',
40 | 'HTTP_HOST' => 'iam.amazonaws.com',
41 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
42 | 'REQUEST_URI' => '/path?query=string'
43 | ];
44 | $requestBody = 'BODY';
45 | $helper = new RequestHelper($serverVars, $requestBody, 'Authorization', 'X-Ems-Date');
46 | $this->assertEquals($requestBody, $helper->getRequestBody());
47 | $expectedHeaders = [
48 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
49 | 'host' => 'iam.amazonaws.com',
50 | ];
51 | $this->assertEqualMaps($expectedHeaders, $helper->getHeaderList());
52 | }
53 |
54 | /**
55 | * @test
56 | * @dataProvider headerNames
57 | */
58 | public function itShouldParseAuthorizationHeader($authHeaderName, $dateHeaderName)
59 | {
60 | $headerList = [
61 | 'host' => 'iam.amazonaws.com',
62 | $dateHeaderName => '20110909T233600Z',
63 | $authHeaderName => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
64 | ];
65 | $authHeader = AuthElements::parseFromHeaders($headerList, $authHeaderName, $dateHeaderName, 'EMS');
66 |
67 | $this->assertEquals(new DateTime('20110909T233600Z', new DateTimeZone('GMT')), $authHeader->getDateTime());
68 | $this->assertEquals('AKIDEXAMPLE', $authHeader->getAccessKeyId());
69 | $this->assertEquals('20110909', $authHeader->getShortDate());
70 | $this->assertEquals('us-east-1/iam/aws4_request', $authHeader->getCredentialScope());
71 | $this->assertEquals(['content-type', 'host', 'x-ems-date'], $authHeader->getSignedHeaders());
72 | $this->assertEquals('f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
73 | $authHeader->getSignature());
74 | }
75 |
76 | public function headerNames()
77 | {
78 | return [
79 | 'default' => ['authorization', 'date'],
80 | 'upcase' => ['Authorization', 'Date'],
81 | 'custom' => ['x-ems-auth', 'x-ems-date'],
82 | 'custom upcase' => ['X-Ems-Auth', 'X-Ems-Date'],
83 | ];
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/test/Escher/Test/Unit/RequestCanonicalizerTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($query, $result);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/Escher/Test/Unit/SignRequestUsingHeaderTest.php:
--------------------------------------------------------------------------------
1 | 'application/x-www-form-urlencoded; charset=utf-8',
19 | 'host' => 'iam.amazonaws.com',
20 | ];
21 | $expectedHeaders = [
22 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
23 | 'host' => 'iam.amazonaws.com',
24 | 'x-ems-date' => '20110909T233600Z',
25 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
26 | ];
27 | $headersToSign = ['content-type', 'host', 'x-ems-date'];
28 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->signRequest(
29 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
30 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
31 | $this->getDate()
32 | );
33 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
34 | }
35 |
36 | /**
37 | * @test
38 | * @group sign_request
39 | */
40 | public function itShouldSignRequestWithUppercaseHeader()
41 | {
42 | $inputHeaders = [
43 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
44 | 'host' => 'iam.amazonaws.com',
45 | 'TEST' => 'TEST message'
46 | ];
47 | $expectedHeaders = [
48 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
49 | 'host' => 'iam.amazonaws.com',
50 | 'x-ems-date' => '20110909T233600Z',
51 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;test;x-ems-date, Signature=f6ae6c5a72056a6f9ad42a9bbfebb868243b4fe451c38b2817739f75c197d26f',
52 | 'test' => 'TEST message',
53 | ];
54 | $headersToSign = ['content-type', 'host', 'x-ems-date', 'TEST'];
55 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->signRequest(
56 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
57 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
58 | $this->getDate()
59 | );
60 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
61 | }
62 |
63 | /**
64 | * @test
65 | * @group sign_request
66 | */
67 | public function itShouldAutomagicallyAddHostHeader()
68 | {
69 | $inputHeaders = [
70 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
71 | ];
72 | $expectedHeaders = [
73 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
74 | 'host' => 'iam.amazonaws.com',
75 | 'x-ems-date' => '20110909T233600Z',
76 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
77 | ];
78 | $headersToSign = ['content-type', 'host', 'x-ems-date'];
79 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->signRequest(
80 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
81 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
82 | $this->getDate()
83 | );
84 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
85 | }
86 |
87 | /**
88 | * @test
89 | * @group sign_request
90 | * @dataProvider urlAndHostProvider
91 | */
92 | public function itShouldAutomagicallyAddHostHeaderWithPort($url, $expectedHost)
93 | {
94 | $inputHeaders = [
95 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
96 | ];
97 | $headersToSign = ['content-type', 'host', 'x-ems-date'];
98 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->signRequest(
99 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
100 | 'POST', $url, 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign, $this->getDate()
101 | );
102 | $this->assertEquals($expectedHost, $actualHeaders['host']);
103 | }
104 |
105 | public function urlAndHostProvider()
106 | {
107 | return [
108 | 'http - custom port' => ['http://iam.amazonaws.com:5000/', 'iam.amazonaws.com:5000'],
109 | 'https - custom port' => ['https://iam.amazonaws.com:5000/', 'iam.amazonaws.com:5000'],
110 |
111 | 'http - default port' => ['http://iam.amazonaws.com:80/', 'iam.amazonaws.com'],
112 | 'https - default port' => ['https://iam.amazonaws.com:443/', 'iam.amazonaws.com'],
113 |
114 | 'http - https port' => ['http://iam.amazonaws.com:443/', 'iam.amazonaws.com:443'],
115 | 'https - http port' => ['https://iam.amazonaws.com:80/', 'iam.amazonaws.com:80']
116 | ];
117 | }
118 |
119 | /**
120 | * @test
121 | * @group sign_request
122 | */
123 | public function itShouldAutomagicallyAddDateAndHostToSignedHeaders()
124 | {
125 | $inputHeaders = [
126 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
127 | ];
128 | $expectedHeaders = [
129 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
130 | 'host' => 'iam.amazonaws.com',
131 | 'x-ems-date' => '20110909T233600Z',
132 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
133 | ];
134 | $headersToSign = ['content-type'];
135 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->signRequest(
136 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
137 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
138 | $this->getDate()
139 | );
140 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
141 | }
142 |
143 | /**
144 | * @test
145 | * @group sign_request
146 | */
147 | public function itShouldOnlySignHeadersExplicitlySetToBeSigned()
148 | {
149 | $inputHeaders = [
150 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
151 | 'x-a-header' => 'that/should/not/be/signed',
152 | ];
153 | $expectedHeaders = [
154 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
155 | 'host' => 'iam.amazonaws.com',
156 | 'x-a-header' => 'that/should/not/be/signed',
157 | 'x-ems-date' => '20110909T233600Z',
158 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
159 | ];
160 |
161 | $headersToSign = ['content-type', 'host', 'x-ems-date'];
162 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->signRequest(
163 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
164 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
165 | $this->getDate()
166 | );
167 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
168 | }
169 |
170 | /**
171 | * @test
172 | * @group sign_request
173 | */
174 | public function itShouldUseTheProvidedAuthHeaderName()
175 | {
176 | $inputHeaders = [
177 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
178 | ];
179 | $expectedHeaders = [
180 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
181 | 'host' => 'iam.amazonaws.com',
182 | 'x-ems-date' => '20110909T233600Z',
183 | 'custom-auth-header' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
184 | ];
185 |
186 | $headersToSign = ['content-type', 'host', 'x-ems-date'];
187 | $actualHeaders = $this->createEscher('us-east-1/iam/aws4_request')->setAuthHeaderKey('Custom-Auth-Header')->signRequest(
188 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
189 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
190 | $this->getDate()
191 | );
192 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
193 | }
194 |
195 | /**
196 | * @test
197 | * @group sign_request
198 | */
199 | public function itShouldUseTheProvidedAlgoPrefix()
200 | {
201 | $inputHeaders = [
202 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
203 | ];
204 | $expectedHeaders = [
205 | 'content-type' => 'application/x-www-form-urlencoded; charset=utf-8',
206 | 'host' => 'iam.amazonaws.com',
207 | 'x-ems-date' => '20110909T233600Z',
208 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd',
209 | ];
210 |
211 | $escher = $this->createEscher('us-east-1/iam/aws4_request');
212 | $headersToSign = ['content-type', 'host', 'x-ems-date'];
213 | $actualHeaders = $escher->signRequest(
214 | 'AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
215 | 'POST', 'http://iam.amazonaws.com/', 'Action=ListUsers&Version=2010-05-08', $inputHeaders, $headersToSign,
216 | $this->getDate()
217 | );
218 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
219 | }
220 |
221 | /**
222 | * @test
223 | * @group sign_request
224 | */
225 | public function itShouldGenerateSignedHeaders()
226 | {
227 | $inputHeaders = [
228 | 'Some-Custom-Header' => 'FooBar'
229 | ];
230 |
231 | $date = new DateTime('2011/05/11 12:00:00', new DateTimeZone("UTC"));
232 | $escher = $this->createEscher('us-east-1/host/aws4_request');
233 |
234 | $actualHeaders = $escher->signRequest(
235 | 'th3K3y', 'very_secure',
236 | 'GET', 'http://example.com/something', '', $inputHeaders, [], $date
237 | );
238 |
239 | $expectedHeaders = [
240 | 'host' => 'example.com',
241 | 'some-custom-header' => 'FooBar',
242 | 'x-ems-date' => '20110511T120000Z',
243 | 'x-ems-auth' => 'EMS-HMAC-SHA256 Credential=th3K3y/20110511/us-east-1/host/aws4_request, SignedHeaders=host;x-ems-date, Signature=e7c1c7b2616d27ecbe3cd81ed3464ea4f6e2a11ad6f7792b23d67f7867e9abb4',
244 | ];
245 |
246 | $this->assertEqualMaps($expectedHeaders, $actualHeaders);
247 | }
248 |
249 | protected function getDate(): DateTime
250 | {
251 | return new DateTime('20110909T233600Z', new DateTimeZone('UTC'));
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/test/Escher/Test/Unit/SignRequestUsingQueryStringTest.php:
--------------------------------------------------------------------------------
1 | createEscher()->presignUrl('th3K3y', 'very_secure',
19 | 'http://example.com/something?foo=bar&baz=barbaz', $this->expires, $this->getDate());
20 |
21 | $expectedSignedUrl = 'http://example.com/something?foo=bar&baz=barbaz&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=th3K3y%2F20110511%2Fus-east-1%2Fhost%2Faws4_request&X-EMS-Date=20110511T120000Z&X-EMS-Expires=123456&X-EMS-SignedHeaders=host&X-EMS-Signature=fbc9dbb91670e84d04ad2ae7505f4f52ab3ff9e192b8233feeae57e9022c2b67';
22 |
23 | $this->assertEquals($expectedSignedUrl, $signedUrl);
24 | }
25 |
26 | /**
27 | * @test
28 | */
29 | public function itShouldHandlePort()
30 | {
31 | $signedUrl = $this->createEscher()->presignUrl('th3K3y', 'very_secure',
32 | 'http://example.com:5000/something?foo=bar&baz=barbaz', $this->expires, $this->getDate());
33 |
34 | $expectedSignedUrl = 'http://example.com:5000/something?foo=bar&baz=barbaz&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=th3K3y%2F20110511%2Fus-east-1%2Fhost%2Faws4_request&X-EMS-Date=20110511T120000Z&X-EMS-Expires=123456&X-EMS-SignedHeaders=host&X-EMS-Signature=7f7032b393945a0167fe65d35a7e2827a781ecab9019d814adf95c23bfa5e458';
35 |
36 | $this->assertEquals($expectedSignedUrl, $signedUrl);
37 | }
38 |
39 | /**
40 | * @test
41 | */
42 | public function itShouldRespectWhenUrlHasSpecialChars()
43 | {
44 | $date = new DateTime('20150310T173248Z', new DateTimeZone('GMT'));
45 | $signedUrl = $this->createEscher('eu/service/ems_request')->presignUrl(
46 | 'service_api_key',
47 | 'service_secret',
48 | 'https://service.example.com/login?id=12345678&domain=login.example.com&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name%3F',
49 | \Escher\Escher::DEFAULT_EXPIRES,
50 | $date
51 | );
52 |
53 | $expectedSignedUrl = 'https://service.example.com/login?id=12345678&domain=login.example.com&redirect_to=https%3A%2F%2Fhome.dev%2Fbootstrap.php%3Fr%3Dservice%2Findex%26service%3Dservice_name%3F&X-EMS-Algorithm=EMS-HMAC-SHA256&X-EMS-Credentials=service_api_key%2F20150310%2Feu%2Fservice%2Fems_request&X-EMS-Date=20150310T173248Z&X-EMS-Expires=86400&X-EMS-SignedHeaders=host&X-EMS-Signature=661f2147c77b6784be5a60a8b842a96de6327653f1ed5d4305da43103c69a6f5';
54 |
55 | $this->assertEquals($expectedSignedUrl, $signedUrl);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------