├── .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 [![Build Status](https://travis-ci.org/emartech/escher-php.svg?branch=master)](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 | --------------------------------------------------------------------------------